Compare commits
4 Commits
v1.1.9
...
feat/relat
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0e06b8d34c | ||
![]() |
b972aa1663 | ||
![]() |
3bf81ee0fa | ||
![]() |
486230a335 |
5
.github/workflows/build.yml
vendored
5
.github/workflows/build.yml
vendored
@ -37,12 +37,9 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
run: pnpm build --standalone
|
run: pnpm build --standalone
|
||||||
|
|
||||||
- name: Generate plugin list
|
|
||||||
run: pnpm generatePluginJson dist/plugins.json
|
|
||||||
|
|
||||||
- name: Clean up obsolete files
|
- name: Clean up obsolete files
|
||||||
run: |
|
run: |
|
||||||
rm -rf dist/*-unpacked Vencord.user.css vencordDesktopRenderer.css vencordDesktopRenderer.css.map
|
rm -rf dist/extension* Vencord.user.css
|
||||||
|
|
||||||
- name: Get some values needed for the release
|
- name: Get some values needed for the release
|
||||||
id: release_values
|
id: release_values
|
||||||
|
5
.github/workflows/publish.yml
vendored
5
.github/workflows/publish.yml
vendored
@ -35,15 +35,15 @@ jobs:
|
|||||||
|
|
||||||
- name: Publish extension
|
- name: Publish extension
|
||||||
run: |
|
run: |
|
||||||
|
cd dist/extension-unpacked
|
||||||
|
|
||||||
# Do not fail so that even if chrome fails, firefox gets a shot. But also store exit code to fail workflow later
|
# Do not fail so that even if chrome fails, firefox gets a shot. But also store exit code to fail workflow later
|
||||||
EXIT_CODE=0
|
EXIT_CODE=0
|
||||||
|
|
||||||
# Chrome
|
# Chrome
|
||||||
cd dist/chromium-unpacked
|
|
||||||
pnpx chrome-webstore-upload-cli@2.1.0 upload --auto-publish || EXIT_CODE=$?
|
pnpx chrome-webstore-upload-cli@2.1.0 upload --auto-publish || EXIT_CODE=$?
|
||||||
|
|
||||||
# Firefox
|
# Firefox
|
||||||
cd ../firefox-unpacked
|
|
||||||
npm i -g web-ext@7.4.0 web-ext-submit@7.4.0
|
npm i -g web-ext@7.4.0 web-ext-submit@7.4.0
|
||||||
web-ext-submit || EXIT_CODE=$?
|
web-ext-submit || EXIT_CODE=$?
|
||||||
|
|
||||||
@ -58,3 +58,4 @@ jobs:
|
|||||||
# Firefox
|
# Firefox
|
||||||
WEB_EXT_API_KEY: ${{ secrets.WEBEXT_USER }}
|
WEB_EXT_API_KEY: ${{ secrets.WEBEXT_USER }}
|
||||||
WEB_EXT_API_SECRET: ${{ secrets.WEBEXT_SECRET }}
|
WEB_EXT_API_SECRET: ${{ secrets.WEBEXT_SECRET }}
|
||||||
|
|
||||||
|
4
.github/workflows/reportBrokenPlugins.yml
vendored
4
.github/workflows/reportBrokenPlugins.yml
vendored
@ -36,7 +36,7 @@ jobs:
|
|||||||
export PATH="$PWD/node_modules/.bin:$PATH"
|
export PATH="$PWD/node_modules/.bin:$PATH"
|
||||||
export CHROMIUM_BIN=$(which chromium-browser)
|
export CHROMIUM_BIN=$(which chromium-browser)
|
||||||
|
|
||||||
esbuild scripts/generateReport.ts > dist/report.mjs
|
esbuild test/generateReport.ts > dist/report.mjs
|
||||||
node dist/report.mjs >> $GITHUB_STEP_SUMMARY
|
node dist/report.mjs >> $GITHUB_STEP_SUMMARY
|
||||||
env:
|
env:
|
||||||
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
|
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
|
||||||
@ -50,7 +50,7 @@ jobs:
|
|||||||
export CHROMIUM_BIN=$(which chromium-browser)
|
export CHROMIUM_BIN=$(which chromium-browser)
|
||||||
export USE_CANARY=true
|
export USE_CANARY=true
|
||||||
|
|
||||||
esbuild scripts/generateReport.ts > dist/report.mjs
|
esbuild test/generateReport.ts > dist/report.mjs
|
||||||
node dist/report.mjs >> $GITHUB_STEP_SUMMARY
|
node dist/report.mjs >> $GITHUB_STEP_SUMMARY
|
||||||
env:
|
env:
|
||||||
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
|
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
|
||||||
|
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -15,7 +15,7 @@ jobs:
|
|||||||
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
|
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
|
||||||
|
|
||||||
- name: Use Node.js 18
|
- name: Use Node.js 18
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 18
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
# Code of Conduct
|
|
||||||
|
|
||||||
Our community is welcoming to everyone, regardless of their characteristics.
|
|
||||||
|
|
||||||
As such, we expect you to treat everyone with respect and contribute to an open and welcoming community.
|
|
||||||
|
|
||||||
DO
|
|
||||||
- have empathy and be nice to others
|
|
||||||
- be respectful of differing opinions, even if you disagree
|
|
||||||
- give and accept constructive criticism
|
|
||||||
|
|
||||||
DON'T
|
|
||||||
- use offensive or derogatory language
|
|
||||||
- troll or spam
|
|
||||||
- personally attack or harass others
|
|
||||||
|
|
||||||
Repetitive violations of these guidelines might get your access to the repository restricted.
|
|
||||||
|
|
||||||
|
|
||||||
If you feel like a user is violating these guidelines or feel treated unfairly, please refrain from publicly challenging them and instead contact a Moderator on our Discord server or send an email to vendicated+conduct@riseup.net!
|
|
17
README.md
17
README.md
@ -4,14 +4,12 @@ The cutest Discord client mod
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Super easy to install (Download Installer, open, click install button, done)
|
- Super easy to install (one click installer)
|
||||||
- 100+ plugins built in: [See a list](https://gist.github.com/Vendicated/8696cde7b92548064a3ae92ead84d033)
|
- 90+ plugins built in: [See a list](https://gist.github.com/Vendicated/8696cde7b92548064a3ae92ead84d033)
|
||||||
- Some highlights: SpotifyControls, GameActivityToggle, Experiments, NoTrack, MessageLogger, QuickReply, Free Emotes/Stickers, CustomCommands, ShowHiddenChannels, PronounDB
|
- Some highlights: SpotifyControls, Experiments, NoTrack, MessageLogger, QuickReply, Free Emotes/Stickers, CustomCommands, ShowHiddenChannels, PronounDB
|
||||||
- Fairly lightweight despite the many inbuilt plugins
|
|
||||||
- Excellent Browser Support: Run Vencord in your Browser via extension or UserScript
|
- Excellent Browser Support: Run Vencord in your Browser via extension or UserScript
|
||||||
- Works on any Discord branch: Stable, Canary or PTB all work (though for the best experience I recommend stable!)
|
|
||||||
- Custom CSS and Themes: Inbuilt css editor with support to import any css files (including BetterDiscord themes)
|
- Custom CSS and Themes: Inbuilt css editor with support to import any css files (including BetterDiscord themes)
|
||||||
- Privacy friendly, blocks Discord analytics & crash reporting out of the box and has no telemetry
|
- Works in all Electron versions (Confirmed working on versions 13-23)
|
||||||
- Maintained very actively, broken plugins are usually fixed within 12 hours
|
- Maintained very actively, broken plugins are usually fixed within 12 hours
|
||||||
|
|
||||||
## Installing / Uninstalling
|
## Installing / Uninstalling
|
||||||
@ -22,7 +20,7 @@ The cutest Discord client mod
|
|||||||
|
|
||||||
[](https://addons.mozilla.org/en-GB/firefox/addon/vencord-web/) [](https://chrome.google.com/webstore/detail/vencord-web/cbghhgpcnddeihccjmnadmkaejncjndb)
|
[](https://addons.mozilla.org/en-GB/firefox/addon/vencord-web/) [](https://chrome.google.com/webstore/detail/vencord-web/cbghhgpcnddeihccjmnadmkaejncjndb)
|
||||||
|
|
||||||
Or use the [UserScript](https://raw.githubusercontent.com/Vencord/builds/main/Vencord.user.js) - Please note that the CSS Editor, Themes loaded from remote sources and co. will not work in the UserScript. Use the extension if you need any of those
|
Or use the [UserScript](https://raw.githubusercontent.com/Vencord/builds/main/Vencord.user.js) - Please note that QuickCSS and plugins making use of external resources will not work with the UserScript.
|
||||||
|
|
||||||
## Building from Source
|
## Building from Source
|
||||||
|
|
||||||
@ -41,8 +39,3 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) and [Megu's Plugin Guide!](docs/2_PLUGINS
|
|||||||
[join]: https://discord.gg/D9uwnFnqmd
|
[join]: https://discord.gg/D9uwnFnqmd
|
||||||
|
|
||||||
[join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join]
|
[join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join]
|
||||||
|
|
||||||
## Disclaimer
|
|
||||||
|
|
||||||
Discord is trademark of Discord Inc. and solely mentioned for the sake of descriptivity.
|
|
||||||
Mention of it does not imply any affiliation with or endorsement by Discord Inc.
|
|
||||||
|
@ -59,8 +59,8 @@ async function checkCors(url, method) {
|
|||||||
const origin = headers["access-control-allow-origin"];
|
const origin = headers["access-control-allow-origin"];
|
||||||
if (origin !== "*" && origin !== window.location.origin) return false;
|
if (origin !== "*" && origin !== window.location.origin) return false;
|
||||||
|
|
||||||
const methods = headers["access-control-allow-methods"]?.toLowerCase().split(/,\s/g);
|
const methods = headers["access-control-allow-methods"]?.split(/,\s/g);
|
||||||
if (methods && !methods.includes(method.toLowerCase())) return false;
|
if (methods && !methods.includes(method)) return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -92,7 +92,6 @@ function GM_fetch(url, opt) {
|
|||||||
resp.arrayBuffer = () => blobTo("arrayBuffer", blob);
|
resp.arrayBuffer = () => blobTo("arrayBuffer", blob);
|
||||||
resp.text = () => blobTo("text", blob);
|
resp.text = () => blobTo("text", blob);
|
||||||
resp.json = async () => JSON.parse(await blobTo("text", blob));
|
resp.json = async () => JSON.parse(await blobTo("text", blob));
|
||||||
resp.headers = new Headers(parseHeaders(resp.responseHeaders));
|
|
||||||
resolve(resp);
|
resolve(resp);
|
||||||
};
|
};
|
||||||
options.ontimeout = () => reject("fetch timeout");
|
options.ontimeout = () => reject("fetch timeout");
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
/**
|
|
||||||
* @template T
|
|
||||||
* @param {T[]} arr
|
|
||||||
* @param {(v: T) => boolean} predicate
|
|
||||||
*/
|
|
||||||
function removeFirst(arr, predicate) {
|
|
||||||
const idx = arr.findIndex(predicate);
|
|
||||||
if (idx !== -1) arr.splice(idx, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
chrome.webRequest.onHeadersReceived.addListener(
|
|
||||||
({ responseHeaders, type, url }) => {
|
|
||||||
if (!responseHeaders) return;
|
|
||||||
|
|
||||||
if (type === "main_frame") {
|
|
||||||
// In main frame requests, the CSP needs to be removed to enable fetching of custom css
|
|
||||||
// as desired by the user
|
|
||||||
removeFirst(responseHeaders, h => h.name.toLowerCase() === "content-security-policy");
|
|
||||||
} else if (type === "stylesheet" && url.startsWith("https://raw.githubusercontent.com")) {
|
|
||||||
// Most users will load css from GitHub, but GitHub doesn't set the correct content type,
|
|
||||||
// so we fix it here
|
|
||||||
removeFirst(responseHeaders, h => h.name.toLowerCase() === "content-type");
|
|
||||||
responseHeaders.push({
|
|
||||||
name: "Content-Type",
|
|
||||||
value: "text/css"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return { responseHeaders };
|
|
||||||
},
|
|
||||||
{ urls: ["https://raw.githubusercontent.com/*", "*://*.discord.com/*"], types: ["main_frame", "stylesheet"] },
|
|
||||||
["blocking", "responseHeaders"]
|
|
||||||
);
|
|
BIN
browser/icon.png
BIN
browser/icon.png
Binary file not shown.
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 21 KiB |
@ -21,8 +21,7 @@
|
|||||||
{
|
{
|
||||||
"run_at": "document_start",
|
"run_at": "document_start",
|
||||||
"matches": ["*://*.discord.com/*"],
|
"matches": ["*://*.discord.com/*"],
|
||||||
"js": ["content.js"],
|
"js": ["content.js"]
|
||||||
"all_frames": true
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -1,41 +0,0 @@
|
|||||||
{
|
|
||||||
"manifest_version": 2,
|
|
||||||
"minimum_chrome_version": "91",
|
|
||||||
|
|
||||||
"name": "Vencord Web",
|
|
||||||
"description": "The cutest Discord mod now in your browser",
|
|
||||||
"author": "Vendicated",
|
|
||||||
"homepage_url": "https://github.com/Vendicated/Vencord",
|
|
||||||
"icons": {
|
|
||||||
"128": "icon.png"
|
|
||||||
},
|
|
||||||
|
|
||||||
"permissions": [
|
|
||||||
"webRequest",
|
|
||||||
"webRequestBlocking",
|
|
||||||
"*://*.discord.com/*",
|
|
||||||
"https://raw.githubusercontent.com/*"
|
|
||||||
],
|
|
||||||
|
|
||||||
"content_scripts": [
|
|
||||||
{
|
|
||||||
"run_at": "document_start",
|
|
||||||
"matches": ["*://*.discord.com/*"],
|
|
||||||
"js": ["content.js"],
|
|
||||||
"all_frames": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
"background": {
|
|
||||||
"scripts": ["background.js"]
|
|
||||||
},
|
|
||||||
|
|
||||||
"web_accessible_resources": ["dist/Vencord.js", "dist/Vencord.css"],
|
|
||||||
|
|
||||||
"browser_specific_settings": {
|
|
||||||
"gecko": {
|
|
||||||
"id": "vencord-firefox@vendicated.dev",
|
|
||||||
"strict_min_version": "91.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -15,7 +15,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"condition": {
|
"condition": {
|
||||||
"resourceTypes": ["main_frame", "sub_frame"]
|
"resourceTypes": ["main_frame"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -13,6 +13,12 @@ Welcome to Megu's Installation Guide! In this file, you will learn about how to
|
|||||||
- [Installing Vencord](#installing-vencord)
|
- [Installing Vencord](#installing-vencord)
|
||||||
- [Updating Vencord](#updating-vencord)
|
- [Updating Vencord](#updating-vencord)
|
||||||
- [Uninstalling Vencord](#uninstalling-vencord)
|
- [Uninstalling Vencord](#uninstalling-vencord)
|
||||||
|
- [Manually Installing Vencord](#manually-installing-vencord)
|
||||||
|
- [On Windows](#on-windows)
|
||||||
|
- [On Linux](#on-linux)
|
||||||
|
- [On MacOS](#on-macos)
|
||||||
|
- [Manual Patching](#manual-patching)
|
||||||
|
- [Manually Uninstalling Vencord](#manually-uninstalling-vencord)
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
@ -21,16 +27,16 @@ Welcome to Megu's Installation Guide! In this file, you will learn about how to
|
|||||||
|
|
||||||
## Installing Vencord
|
## Installing Vencord
|
||||||
|
|
||||||
|
> :exclamation: If this doesn't work, see [Manually Installing Vencord](#manually-installing-vencord)
|
||||||
|
|
||||||
Install `pnpm`:
|
Install `pnpm`:
|
||||||
|
|
||||||
> :exclamation: This next command may need to be run as admin/root depending on your system, and you may need to close and reopen your terminal for pnpm to be in your PATH.
|
> :exclamation: this may need to be run as admin depending on your system, and you may need to close and reopen your terminal.
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
npm i -g pnpm
|
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:
|
Clone Vencord:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
@ -95,4 +101,102 @@ Simply run:
|
|||||||
pnpm uninject
|
pnpm uninject
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The above command may ask you to also run:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
pnpm uninject
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manually Installing Vencord
|
||||||
|
|
||||||
|
- [Windows](#on-windows)
|
||||||
|
- [Linux](#on-linux)
|
||||||
|
- [MacOS](#on-macos)
|
||||||
|
|
||||||
|
### On Windows
|
||||||
|
|
||||||
|
Press Win+R and enter: `%LocalAppData%` and hit enter. In this page, find the page (Discord, DiscordPTB, DiscordCanary, etc) that you want to patch.
|
||||||
|
|
||||||
|
Now follow the instructions at [Manual Patching](#manual-patching)
|
||||||
|
|
||||||
|
### On Linux
|
||||||
|
|
||||||
|
The Discord folder is usually in one of the following paths:
|
||||||
|
|
||||||
|
- /usr/share
|
||||||
|
- /usr/lib64
|
||||||
|
- /opt
|
||||||
|
- /home/$USER/.local/share
|
||||||
|
|
||||||
|
If you use flatpak, it will usually be in one of the following paths:
|
||||||
|
|
||||||
|
- /var/lib/flatpak/app/com.discordapp.Discord/current/active/files
|
||||||
|
- /home/$USER/.local/share/flatpak/app/com.discordapp.Discord/current/active/files
|
||||||
|
|
||||||
|
You will need to give flatpak access to vencord with one of the following commands:
|
||||||
|
|
||||||
|
> :exclamation: If not on stable, replace `com.discordapp.Discord` with your branch name, e.g., `com.discordapp.DiscordCanary`
|
||||||
|
|
||||||
|
> :exclamation: Replace `/path/to/vencord/` with the path to your vencord folder (NOT the dist folder)
|
||||||
|
|
||||||
|
If Discord flatpak install is in /home/:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
flatpak override --user com.discordapp.Discord --filesystem="/path/to/vencord/"
|
||||||
|
```
|
||||||
|
|
||||||
|
If Discord flatpak install not in /home/:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
sudo flatpak override com.discordapp.Discord --filesystem="/path/to/vencord"
|
||||||
|
```
|
||||||
|
|
||||||
|
Now follow the instructions at [Manual Patching](#manual-patching)
|
||||||
|
|
||||||
|
### On MacOS
|
||||||
|
|
||||||
|
Open finder and go to your Applications folder. Right-Click on the Discord application you want to patch, and view contents.
|
||||||
|
|
||||||
|
Go to the `Contents/Resources` folder.
|
||||||
|
|
||||||
|
Now follow the instructions at [Manual Patching](#manual-patching)
|
||||||
|
|
||||||
|
### Manual Patching
|
||||||
|
|
||||||
|
> :exclamation: If using Flatpak on linux, go to the folder that contains the `app.asar` file, and skip to where we create the `app` folder below.
|
||||||
|
|
||||||
|
> :exclamation: On Linux/MacOS, there's a chance there won't be an `app-<number>` folder, but there probably is a `resources` folder, so keep reading :)
|
||||||
|
|
||||||
|
Inside there, look for the `app-<number>` folders. If you have multiple, use the highest number. If that doesn't work, do it for the rest of the `app-<number>` folders.
|
||||||
|
|
||||||
|
Inside there, go to the `resources` folder. There should be a file called `app.asar`. If there isn't, look at a different `app-<number>` folder instead.
|
||||||
|
|
||||||
|
Make a new folder in `resources` called `app`. In here, we will make two files:
|
||||||
|
|
||||||
|
`package.json` and `index.js`
|
||||||
|
|
||||||
|
In `index.js`:
|
||||||
|
|
||||||
|
> :exclamation: Replace the path in the first line with the path to `patcher.js` in your vencord dist folder.
|
||||||
|
> On Windows, you can get this by shift-rightclicking the patcher.js file and selecting "copy as path"
|
||||||
|
|
||||||
|
```js
|
||||||
|
require("C:/Users/<your user>/path/to/vencord/dist/patcher.js");
|
||||||
|
```
|
||||||
|
|
||||||
|
And in `package.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "name": "discord", "main": "index.js" }
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, fully close & reopen your Discord client and check to see that `Vencord` appears in settings!
|
||||||
|
|
||||||
|
### Manually Uninstalling Vencord
|
||||||
|
|
||||||
|
> :exclamation: Do not delete `app.asar` - Only delete the `app` folder we created.
|
||||||
|
|
||||||
|
Use the instructions above to find the `app` folder, and delete it. Then Close & Reopen Discord.
|
||||||
|
|
||||||
If you need more help, ask in the support channel in our [Discord Server](https://discord.gg/D9uwnFnqmd).
|
If you need more help, ask in the support channel in our [Discord Server](https://discord.gg/D9uwnFnqmd).
|
||||||
|
@ -26,10 +26,6 @@ export default definePlugin({
|
|||||||
name: "Your Name",
|
name: "Your Name",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
// Delete `patches` if you are not using code patches, as it will make
|
|
||||||
// your plugin require restarts, and your stop() method will not be
|
|
||||||
// invoked at all. The presence of the key in the object alone is
|
|
||||||
// enough to trigger this behavior, even if the value is an empty array.
|
|
||||||
patches: [],
|
patches: [],
|
||||||
// Delete these two below if you are only using code patches
|
// Delete these two below if you are only using code patches
|
||||||
start() {},
|
start() {},
|
||||||
|
18
package.json
18
package.json
@ -1,8 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "vencord",
|
"name": "vencord",
|
||||||
"private": "true",
|
"private": "true",
|
||||||
"version": "1.1.9",
|
"version": "1.0.6",
|
||||||
"description": "The cutest Discord client mod",
|
"description": "The cutest Discord client mod",
|
||||||
|
"keywords": [],
|
||||||
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/Vendicated/Vencord/issues"
|
"url": "https://github.com/Vendicated/Vencord/issues"
|
||||||
@ -19,10 +20,9 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node scripts/build/build.mjs",
|
"build": "node scripts/build/build.mjs",
|
||||||
"buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs",
|
"buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs",
|
||||||
"generatePluginJson": "tsx scripts/generatePluginList.ts",
|
|
||||||
"inject": "node scripts/runInstaller.mjs",
|
"inject": "node scripts/runInstaller.mjs",
|
||||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --ignore-pattern src/userplugins",
|
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
||||||
"lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins",
|
"lint-styles": "stylelint \"src/**/*.css\"",
|
||||||
"lint:fix": "pnpm lint --fix",
|
"lint:fix": "pnpm lint --fix",
|
||||||
"test": "pnpm build && pnpm lint && pnpm lint-styles && pnpm testTsc",
|
"test": "pnpm build && pnpm lint && pnpm lint-styles && pnpm testTsc",
|
||||||
"testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc",
|
"testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc",
|
||||||
@ -33,9 +33,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vap/core": "0.0.12",
|
"@vap/core": "0.0.12",
|
||||||
"@vap/shiki": "0.10.3",
|
"@vap/shiki": "0.10.3",
|
||||||
"fflate": "^0.7.4",
|
"fflate": "^0.7.4"
|
||||||
"nanoid": "^4.0.2",
|
|
||||||
"virtual-merge": "^1.0.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/diff": "^5.0.2",
|
"@types/diff": "^5.0.2",
|
||||||
@ -61,11 +59,10 @@
|
|||||||
"standalone-electron-types": "^1.0.0",
|
"standalone-electron-types": "^1.0.0",
|
||||||
"stylelint": "^14.16.1",
|
"stylelint": "^14.16.1",
|
||||||
"stylelint-config-standard": "^29.0.0",
|
"stylelint-config-standard": "^29.0.0",
|
||||||
"tsx": "^3.12.6",
|
|
||||||
"type-fest": "^3.5.3",
|
"type-fest": "^3.5.3",
|
||||||
"typescript": "^4.9.4"
|
"typescript": "^4.9.4"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@8.1.1",
|
"packageManager": "pnpm@7.13.4",
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"eslint-plugin-path-alias@1.0.0": "patches/eslint-plugin-path-alias@1.0.0.patch",
|
"eslint-plugin-path-alias@1.0.0": "patches/eslint-plugin-path-alias@1.0.0.patch",
|
||||||
@ -92,7 +89,6 @@
|
|||||||
"sourceDir": "./dist/extension-v2-unpacked"
|
"sourceDir": "./dist/extension-v2-unpacked"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18",
|
"node": ">=18"
|
||||||
"pnpm": ">=8"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
1329
pnpm-lock.yaml
generated
1329
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -48,7 +48,6 @@ const sourceMapFooter = s => watch ? "" : `//# sourceMappingURL=vencord://${s}.j
|
|||||||
const sourcemap = watch ? "inline" : "external";
|
const sourcemap = watch ? "inline" : "external";
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
// common preload
|
|
||||||
esbuild.build({
|
esbuild.build({
|
||||||
...nodeCommonOpts,
|
...nodeCommonOpts,
|
||||||
entryPoints: ["src/preload.ts"],
|
entryPoints: ["src/preload.ts"],
|
||||||
@ -56,19 +55,12 @@ await Promise.all([
|
|||||||
footer: { js: "//# sourceURL=VencordPreload\n" + sourceMapFooter("preload") },
|
footer: { js: "//# sourceURL=VencordPreload\n" + sourceMapFooter("preload") },
|
||||||
sourcemap,
|
sourcemap,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Discord Desktop main & renderer
|
|
||||||
esbuild.build({
|
esbuild.build({
|
||||||
...nodeCommonOpts,
|
...nodeCommonOpts,
|
||||||
entryPoints: ["src/main/index.ts"],
|
entryPoints: ["src/patcher.ts"],
|
||||||
outfile: "dist/patcher.js",
|
outfile: "dist/patcher.js",
|
||||||
footer: { js: "//# sourceURL=VencordPatcher\n" + sourceMapFooter("patcher") },
|
footer: { js: "//# sourceURL=VencordPatcher\n" + sourceMapFooter("patcher") },
|
||||||
sourcemap,
|
sourcemap,
|
||||||
define: {
|
|
||||||
...defines,
|
|
||||||
IS_DISCORD_DESKTOP: true,
|
|
||||||
IS_VENCORD_DESKTOP: false
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
esbuild.build({
|
esbuild.build({
|
||||||
...commonOpts,
|
...commonOpts,
|
||||||
@ -80,48 +72,12 @@ await Promise.all([
|
|||||||
globalName: "Vencord",
|
globalName: "Vencord",
|
||||||
sourcemap,
|
sourcemap,
|
||||||
plugins: [
|
plugins: [
|
||||||
globPlugins("discordDesktop"),
|
globPlugins,
|
||||||
...commonOpts.plugins
|
...commonOpts.plugins
|
||||||
],
|
],
|
||||||
define: {
|
define: {
|
||||||
...defines,
|
...defines,
|
||||||
IS_WEB: false,
|
IS_WEB: false
|
||||||
IS_DISCORD_DESKTOP: true,
|
|
||||||
IS_VENCORD_DESKTOP: false
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
// Vencord Desktop main & renderer
|
|
||||||
esbuild.build({
|
|
||||||
...nodeCommonOpts,
|
|
||||||
entryPoints: ["src/main/index.ts"],
|
|
||||||
outfile: "dist/vencordDesktopMain.js",
|
|
||||||
footer: { js: "//# sourceURL=VencordDesktopMain\n" + sourceMapFooter("vencordDesktopMain") },
|
|
||||||
sourcemap,
|
|
||||||
define: {
|
|
||||||
...defines,
|
|
||||||
IS_DISCORD_DESKTOP: false,
|
|
||||||
IS_VENCORD_DESKTOP: true
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
esbuild.build({
|
|
||||||
...commonOpts,
|
|
||||||
entryPoints: ["src/Vencord.ts"],
|
|
||||||
outfile: "dist/vencordDesktopRenderer.js",
|
|
||||||
format: "iife",
|
|
||||||
target: ["esnext"],
|
|
||||||
footer: { js: "//# sourceURL=VencordDesktopRenderer\n" + sourceMapFooter("vencordDesktopRenderer") },
|
|
||||||
globalName: "Vencord",
|
|
||||||
sourcemap,
|
|
||||||
plugins: [
|
|
||||||
globPlugins("vencordDesktop"),
|
|
||||||
...commonOpts.plugins
|
|
||||||
],
|
|
||||||
define: {
|
|
||||||
...defines,
|
|
||||||
IS_WEB: false,
|
|
||||||
IS_DISCORD_DESKTOP: false,
|
|
||||||
IS_VENCORD_DESKTOP: true
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
]).catch(err => {
|
]).catch(err => {
|
||||||
|
@ -36,18 +36,16 @@ 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"],
|
||||||
plugins: [
|
plugins: [
|
||||||
globPlugins("web"),
|
globPlugins,
|
||||||
...commonOpts.plugins,
|
...commonOpts.plugins,
|
||||||
],
|
],
|
||||||
target: ["esnext"],
|
target: ["esnext"],
|
||||||
define: {
|
define: {
|
||||||
IS_WEB: "true",
|
IS_WEB: "true",
|
||||||
IS_STANDALONE: "true",
|
IS_STANDALONE: "true",
|
||||||
IS_DEV: JSON.stringify(watch),
|
IS_DEV: JSON.stringify(watch)
|
||||||
IS_DISCORD_DESKTOP: "false",
|
|
||||||
IS_VENCORD_DESKTOP: "false"
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -142,7 +140,6 @@ const appendCssRuntime = readFile("dist/Vencord.user.css", "utf-8").then(content
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
appendCssRuntime,
|
appendCssRuntime,
|
||||||
buildPluginZip("extension.zip", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"], true),
|
buildPluginZip("extension.zip", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"], true),
|
||||||
buildPluginZip("chromium-unpacked", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"], false),
|
buildPluginZip("extension-unpacked", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"], false),
|
||||||
buildPluginZip("firefox-unpacked", ["background.js", "content.js", "manifestv2.json", "icon.png"], false),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -33,8 +33,6 @@ export const banner = {
|
|||||||
`.trim()
|
`.trim()
|
||||||
};
|
};
|
||||||
|
|
||||||
const isWeb = process.argv.slice(0, 2).some(f => f.endsWith("buildWeb.mjs"));
|
|
||||||
|
|
||||||
// https://github.com/evanw/esbuild/issues/619#issuecomment-751995294
|
// https://github.com/evanw/esbuild/issues/619#issuecomment-751995294
|
||||||
/**
|
/**
|
||||||
* @type {import("esbuild").Plugin}
|
* @type {import("esbuild").Plugin}
|
||||||
@ -48,9 +46,9 @@ export const makeAllPackagesExternalPlugin = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {(kind: "web" | "discordDesktop" | "vencordDesktop") => import("esbuild").Plugin}
|
* @type {import("esbuild").Plugin}
|
||||||
*/
|
*/
|
||||||
export const globPlugins = kind => ({
|
export const globPlugins = {
|
||||||
name: "glob-plugins",
|
name: "glob-plugins",
|
||||||
setup: build => {
|
setup: build => {
|
||||||
const filter = /^~plugins$/;
|
const filter = /^~plugins$/;
|
||||||
@ -71,17 +69,9 @@ export const globPlugins = kind => ({
|
|||||||
const files = await readdir(`./src/${dir}`);
|
const files = await readdir(`./src/${dir}`);
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if (file.startsWith(".")) continue;
|
if (file.startsWith(".")) continue;
|
||||||
if (file === "index.ts") continue;
|
if (file === "index.ts") {
|
||||||
const fileBits = file.split(".");
|
continue;
|
||||||
if (fileBits.length > 2 && ["ts", "tsx"].includes(fileBits.at(-1))) {
|
|
||||||
const mod = fileBits.at(-2);
|
|
||||||
if (mod === "dev" && !watch) continue;
|
|
||||||
if (mod === "web" && kind === "discordDesktop") continue;
|
|
||||||
if (mod === "desktop" && kind === "web") continue;
|
|
||||||
if (mod === "discordDesktop" && kind !== "discordDesktop") continue;
|
|
||||||
if (mod === "vencordDesktop" && kind !== "vencordDesktop") continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mod = `p${i}`;
|
const mod = `p${i}`;
|
||||||
code += `import ${mod} from "./${dir}/${file.replace(/\.tsx?$/, "")}";\n`;
|
code += `import ${mod} from "./${dir}/${file.replace(/\.tsx?$/, "")}";\n`;
|
||||||
plugins += `[${mod}.name]:${mod},\n`;
|
plugins += `[${mod}.name]:${mod},\n`;
|
||||||
@ -95,7 +85,7 @@ export const globPlugins = kind => ({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {import("esbuild").Plugin}
|
* @type {import("esbuild").Plugin}
|
||||||
@ -195,7 +185,7 @@ export const commonOpts = {
|
|||||||
legalComments: "linked",
|
legalComments: "linked",
|
||||||
banner,
|
banner,
|
||||||
plugins: [fileIncludePlugin, gitHashPlugin, gitRemotePlugin, stylePlugin],
|
plugins: [fileIncludePlugin, gitHashPlugin, gitRemotePlugin, stylePlugin],
|
||||||
external: ["~plugins", "~git-hash", "~git-remote", "/assets/*"],
|
external: ["~plugins", "~git-hash", "~git-remote"],
|
||||||
inject: ["./scripts/build/inject/react.mjs"],
|
inject: ["./scripts/build/inject/react.mjs"],
|
||||||
jsxFactory: "VencordCreateElement",
|
jsxFactory: "VencordCreateElement",
|
||||||
jsxFragment: "VencordFragment",
|
jsxFragment: "VencordFragment",
|
||||||
|
@ -1,191 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2023 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Dirent, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
||||||
import { access, readFile } from "fs/promises";
|
|
||||||
import { join } from "path";
|
|
||||||
import { BigIntLiteral, createSourceFile, Identifier, isArrayLiteralExpression, isCallExpression, isExportAssignment, isIdentifier, isObjectLiteralExpression, isPropertyAccessExpression, isPropertyAssignment, isStringLiteral, isVariableStatement, NamedDeclaration, NodeArray, ObjectLiteralExpression, ScriptTarget, StringLiteral, SyntaxKind } from "typescript";
|
|
||||||
|
|
||||||
interface Dev {
|
|
||||||
name: string;
|
|
||||||
id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PluginData {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
authors: Dev[];
|
|
||||||
dependencies: string[];
|
|
||||||
hasPatches: boolean;
|
|
||||||
hasCommands: boolean;
|
|
||||||
required: boolean;
|
|
||||||
enabledByDefault: boolean;
|
|
||||||
target: "discordDesktop" | "vencordDesktop" | "web" | "dev";
|
|
||||||
}
|
|
||||||
|
|
||||||
const devs = {} as Record<string, Dev>;
|
|
||||||
|
|
||||||
function getName(node: NamedDeclaration) {
|
|
||||||
return node.name && isIdentifier(node.name) ? node.name.text : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasName(node: NamedDeclaration, name: string) {
|
|
||||||
return getName(node) === name;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getObjectProp(node: ObjectLiteralExpression, name: string) {
|
|
||||||
const prop = node.properties.find(p => hasName(p, name));
|
|
||||||
if (prop && isPropertyAssignment(prop)) return prop.initializer;
|
|
||||||
return prop;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseDevs() {
|
|
||||||
const file = createSourceFile("constants.ts", readFileSync("src/utils/constants.ts", "utf8"), ScriptTarget.Latest);
|
|
||||||
|
|
||||||
for (const child of file.getChildAt(0).getChildren()) {
|
|
||||||
if (!isVariableStatement(child)) continue;
|
|
||||||
|
|
||||||
const devsDeclaration = child.declarationList.declarations.find(d => hasName(d, "Devs"));
|
|
||||||
if (!devsDeclaration?.initializer || !isCallExpression(devsDeclaration.initializer)) continue;
|
|
||||||
|
|
||||||
const value = devsDeclaration.initializer.arguments[0];
|
|
||||||
|
|
||||||
if (!isObjectLiteralExpression(value)) return;
|
|
||||||
|
|
||||||
for (const prop of value.properties) {
|
|
||||||
const name = (prop.name as Identifier).text;
|
|
||||||
const value = isPropertyAssignment(prop) ? prop.initializer : prop;
|
|
||||||
|
|
||||||
if (!isObjectLiteralExpression(value)) throw new Error(`Failed to parse devs: ${name} is not an object literal`);
|
|
||||||
|
|
||||||
devs[name] = {
|
|
||||||
name: (getObjectProp(value, "name") as StringLiteral).text,
|
|
||||||
id: (getObjectProp(value, "id") as BigIntLiteral).text.slice(0, -1)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error("Could not find Devs constant");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function parseFile(fileName: string) {
|
|
||||||
const file = createSourceFile(fileName, await readFile(fileName, "utf8"), ScriptTarget.Latest);
|
|
||||||
|
|
||||||
const fail = (reason: string) => {
|
|
||||||
return new Error(`Invalid plugin ${fileName}, because ${reason}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const node of file.getChildAt(0).getChildren()) {
|
|
||||||
if (!isExportAssignment(node) || !isCallExpression(node.expression)) continue;
|
|
||||||
|
|
||||||
const call = node.expression;
|
|
||||||
if (!isIdentifier(call.expression) || call.expression.text !== "definePlugin") continue;
|
|
||||||
|
|
||||||
const pluginObj = node.expression.arguments[0];
|
|
||||||
if (!isObjectLiteralExpression(pluginObj)) throw fail("no object literal passed to definePlugin");
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
hasPatches: false,
|
|
||||||
hasCommands: false,
|
|
||||||
enabledByDefault: false,
|
|
||||||
required: false,
|
|
||||||
} as PluginData;
|
|
||||||
|
|
||||||
for (const prop of pluginObj.properties) {
|
|
||||||
const key = getName(prop);
|
|
||||||
const value = isPropertyAssignment(prop) ? prop.initializer : prop;
|
|
||||||
|
|
||||||
switch (key) {
|
|
||||||
case "name":
|
|
||||||
case "description":
|
|
||||||
if (!isStringLiteral(value)) throw fail(`${key} is not a string literal`);
|
|
||||||
data[key] = value.text;
|
|
||||||
break;
|
|
||||||
case "patches":
|
|
||||||
data.hasPatches = true;
|
|
||||||
break;
|
|
||||||
case "commands":
|
|
||||||
data.hasCommands = true;
|
|
||||||
break;
|
|
||||||
case "authors":
|
|
||||||
if (!isArrayLiteralExpression(value)) throw fail("authors is not an array literal");
|
|
||||||
data.authors = value.elements.map(e => {
|
|
||||||
if (!isPropertyAccessExpression(e)) throw fail("authors array contains non-property access expressions");
|
|
||||||
return devs[getName(e)!];
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "dependencies":
|
|
||||||
if (!isArrayLiteralExpression(value)) throw fail("dependencies is not an array literal");
|
|
||||||
const { elements } = value;
|
|
||||||
if (elements.some(e => !isStringLiteral(e))) throw fail("dependencies array contains non-string elements");
|
|
||||||
data.dependencies = (elements as NodeArray<StringLiteral>).map(e => e.text);
|
|
||||||
break;
|
|
||||||
case "required":
|
|
||||||
case "enabledByDefault":
|
|
||||||
data[key] = value.kind === SyntaxKind.TrueKeyword;
|
|
||||||
if (!data[key] && value.kind !== SyntaxKind.FalseKeyword) throw fail(`${key} is not a boolean literal`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data.name || !data.description || !data.authors) throw fail("name, description or authors are missing");
|
|
||||||
|
|
||||||
const fileBits = fileName.split(".");
|
|
||||||
if (fileBits.length > 2 && ["ts", "tsx"].includes(fileBits.at(-1)!)) {
|
|
||||||
const mod = fileBits.at(-2)!;
|
|
||||||
if (!["web", "discordDesktop", "vencordDesktop", "dev"].includes(mod)) throw fail(`invalid target ${fileBits.at(-2)}`);
|
|
||||||
data.target = mod as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw fail("no default export called 'definePlugin' found");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getEntryPoint(dirent: Dirent) {
|
|
||||||
const base = join("./src/plugins", dirent.name);
|
|
||||||
if (!dirent.isDirectory()) return base;
|
|
||||||
|
|
||||||
for (const name of ["index.ts", "index.tsx"]) {
|
|
||||||
const full = join(base, name);
|
|
||||||
try {
|
|
||||||
await access(full);
|
|
||||||
return full;
|
|
||||||
} catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`${dirent.name}: Couldn't find entry point`);
|
|
||||||
}
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
parseDevs();
|
|
||||||
const plugins = readdirSync("./src/plugins", { withFileTypes: true }).filter(d => d.name !== "index.ts");
|
|
||||||
|
|
||||||
const promises = plugins.map(async dirent => parseFile(await getEntryPoint(dirent)));
|
|
||||||
|
|
||||||
const data = JSON.stringify(await Promise.all(promises));
|
|
||||||
|
|
||||||
if (process.argv.length > 2) {
|
|
||||||
writeFileSync(process.argv[2], data);
|
|
||||||
} else {
|
|
||||||
console.log(data);
|
|
||||||
}
|
|
||||||
})();
|
|
@ -27,48 +27,20 @@ export { PlainSettings, Settings };
|
|||||||
import "./utils/quickCss";
|
import "./utils/quickCss";
|
||||||
import "./webpack/patchWebpack";
|
import "./webpack/patchWebpack";
|
||||||
|
|
||||||
import { showNotification } from "./api/Notifications";
|
import { popNotice, showNotice } from "./api/Notices";
|
||||||
import { PlainSettings, Settings } from "./api/settings";
|
import { PlainSettings, Settings } from "./api/settings";
|
||||||
import { patches, PMLogger, startAllPlugins } from "./plugins";
|
import { patches, PMLogger, startAllPlugins } from "./plugins";
|
||||||
import { localStorage } from "./utils/localStorage";
|
|
||||||
import { relaunch } from "./utils/native";
|
|
||||||
import { getCloudSettings, putCloudSettings } from "./utils/settingsSync";
|
|
||||||
import { checkForUpdates, rebuild, update, UpdateLogger } from "./utils/updater";
|
import { checkForUpdates, rebuild, update, UpdateLogger } from "./utils/updater";
|
||||||
import { onceReady } from "./webpack";
|
import { onceReady } from "./webpack";
|
||||||
import { SettingsRouter } from "./webpack/common";
|
import { SettingsRouter } from "./webpack/common";
|
||||||
|
|
||||||
export let Components: any;
|
export let Components: any;
|
||||||
|
|
||||||
async function syncSettings() {
|
|
||||||
if (
|
|
||||||
Settings.cloud.settingsSync && // if it's enabled
|
|
||||||
Settings.cloud.authenticated // if cloud integrations are enabled
|
|
||||||
) {
|
|
||||||
if (localStorage.Vencord_settingsDirty) {
|
|
||||||
await putCloudSettings();
|
|
||||||
delete localStorage.Vencord_settingsDirty;
|
|
||||||
} else if (await getCloudSettings(false)) { // if we synchronized something (false means no sync)
|
|
||||||
// we show a notification here instead of allowing getCloudSettings() to show one to declutter the amount of
|
|
||||||
// potential notifications that might occur. getCloudSettings() will always send a notification regardless if
|
|
||||||
// there was an error to notify the user, but besides that we only want to show one notification instead of all
|
|
||||||
// of the possible ones it has (such as when your settings are newer).
|
|
||||||
showNotification({
|
|
||||||
title: "Cloud Settings",
|
|
||||||
body: "Your settings have been updated! Click here to restart to fully apply changes!",
|
|
||||||
color: "var(--green-360)",
|
|
||||||
onClick: relaunch
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
await onceReady;
|
await onceReady;
|
||||||
startAllPlugins();
|
startAllPlugins();
|
||||||
Components = await import("./components");
|
Components = await import("./components");
|
||||||
|
|
||||||
syncSettings();
|
|
||||||
|
|
||||||
if (!IS_WEB) {
|
if (!IS_WEB) {
|
||||||
try {
|
try {
|
||||||
const isOutdated = await checkForUpdates();
|
const isOutdated = await checkForUpdates();
|
||||||
@ -76,28 +48,33 @@ async function init() {
|
|||||||
|
|
||||||
if (Settings.autoUpdate) {
|
if (Settings.autoUpdate) {
|
||||||
await update();
|
await update();
|
||||||
await rebuild();
|
const needsFullRestart = await rebuild();
|
||||||
if (Settings.autoUpdateNotification)
|
setTimeout(() => {
|
||||||
setTimeout(() => showNotification({
|
showNotice(
|
||||||
title: "Vencord has been updated!",
|
"Vencord has been updated!",
|
||||||
body: "Click here to restart",
|
"Restart",
|
||||||
permanent: true,
|
() => {
|
||||||
noPersist: true,
|
if (needsFullRestart)
|
||||||
onClick: relaunch
|
window.DiscordNative.app.relaunch();
|
||||||
}), 10_000);
|
else
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, 10_000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Settings.notifyAboutUpdates)
|
if (Settings.notifyAboutUpdates)
|
||||||
setTimeout(() => showNotification({
|
setTimeout(() => {
|
||||||
title: "A Vencord update is available!",
|
showNotice(
|
||||||
body: "Click here to view the update",
|
"A Vencord update is available!",
|
||||||
permanent: true,
|
"View Update",
|
||||||
noPersist: true,
|
() => {
|
||||||
onClick() {
|
popNotice();
|
||||||
SettingsRouter.open("VencordUpdater");
|
SettingsRouter.open("VencordUpdater");
|
||||||
}
|
}
|
||||||
}), 10_000);
|
);
|
||||||
|
}, 10_000);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
UpdateLogger.error("Failed to check for updates", err);
|
UpdateLogger.error("Failed to check for updates", err);
|
||||||
}
|
}
|
||||||
@ -118,12 +95,3 @@ async function init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|
||||||
if (IS_DISCORD_DESKTOP && Settings.winNativeTitleBar && navigator.platform.toLowerCase().startsWith("win")) {
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
document.head.append(Object.assign(document.createElement("style"), {
|
|
||||||
id: "vencord-native-titlebar-style",
|
|
||||||
textContent: "[class*=titleBar-]{display: none!important}"
|
|
||||||
}));
|
|
||||||
}, { once: true });
|
|
||||||
}
|
|
||||||
|
@ -29,12 +29,11 @@ export enum BadgePosition {
|
|||||||
|
|
||||||
export interface ProfileBadge {
|
export interface ProfileBadge {
|
||||||
/** The tooltip to show on hover. Required for image badges */
|
/** The tooltip to show on hover. Required for image badges */
|
||||||
description?: string;
|
tooltip?: string;
|
||||||
/** Custom component for the badge (tooltip not included) */
|
/** Custom component for the badge (tooltip not included) */
|
||||||
component?: ComponentType<ProfileBadge & BadgeUserArgs>;
|
component?: ComponentType<ProfileBadge & BadgeUserArgs>;
|
||||||
/** The custom image to use */
|
/** The custom image to use */
|
||||||
image?: string;
|
image?: string;
|
||||||
link?: string;
|
|
||||||
/** Action to perform when you click the badge */
|
/** Action to perform when you click the badge */
|
||||||
onClick?(): void;
|
onClick?(): void;
|
||||||
/** Should the user display this badge? */
|
/** Should the user display this badge? */
|
||||||
@ -70,19 +69,17 @@ export function removeBadge(badge: ProfileBadge) {
|
|||||||
* Inject badges into the profile badges array.
|
* Inject badges into the profile badges array.
|
||||||
* You probably don't need to use this.
|
* You probably don't need to use this.
|
||||||
*/
|
*/
|
||||||
export function _getBadges(args: BadgeUserArgs) {
|
export function inject(badgeArray: ProfileBadge[], args: BadgeUserArgs) {
|
||||||
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)) {
|
||||||
badge.position === BadgePosition.START
|
badge.position === BadgePosition.START
|
||||||
? badges.unshift({ ...badge, ...args })
|
? badgeArray.unshift({ ...badge, ...args })
|
||||||
: badges.push({ ...badge, ...args });
|
: badgeArray.push({ ...badge, ...args });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const donorBadge = (Plugins.BadgeAPI as any).getDonorBadge(args.user.id);
|
(Plugins.BadgeAPI as any).addDonorBadge(badgeArray, args.user.id);
|
||||||
if (donorBadge) badges.unshift(donorBadge);
|
|
||||||
|
|
||||||
return badges;
|
return badgeArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BadgeUserArgs {
|
export interface BadgeUserArgs {
|
||||||
|
@ -111,7 +111,6 @@ function registerSubCommands(cmd: Command, plugin: string) {
|
|||||||
...o,
|
...o,
|
||||||
type: ApplicationCommandType.CHAT_INPUT,
|
type: ApplicationCommandType.CHAT_INPUT,
|
||||||
name: `${cmd.name} ${o.name}`,
|
name: `${cmd.name} ${o.name}`,
|
||||||
id: `${o.name}-${cmd.id}`,
|
|
||||||
displayName: `${cmd.name} ${o.name}`,
|
displayName: `${cmd.name} ${o.name}`,
|
||||||
subCommandPath: [{
|
subCommandPath: [{
|
||||||
name: o.name,
|
name: o.name,
|
||||||
|
@ -1,155 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2023 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import Logger from "@utils/Logger";
|
|
||||||
import type { ReactElement } from "react";
|
|
||||||
|
|
||||||
type ContextMenuPatchCallbackReturn = (() => void) | void;
|
|
||||||
/**
|
|
||||||
* @param children The rendered context menu elements
|
|
||||||
* @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example
|
|
||||||
* @returns A callback which is only ran once used to modify the context menu elements (Use to avoid duplicates)
|
|
||||||
*/
|
|
||||||
export type NavContextMenuPatchCallback = (children: Array<React.ReactElement>, ...args: Array<any>) => ContextMenuPatchCallbackReturn;
|
|
||||||
/**
|
|
||||||
* @param navId The navId of the context menu being patched
|
|
||||||
* @param children The rendered context menu elements
|
|
||||||
* @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example
|
|
||||||
* @returns A callback which is only ran once used to modify the context menu elements (Use to avoid duplicates)
|
|
||||||
*/
|
|
||||||
export type GlobalContextMenuPatchCallback = (navId: string, children: Array<React.ReactElement>, ...args: Array<any>) => ContextMenuPatchCallbackReturn;
|
|
||||||
|
|
||||||
const ContextMenuLogger = new Logger("ContextMenu");
|
|
||||||
|
|
||||||
export const navPatches = new Map<string, Set<NavContextMenuPatchCallback>>();
|
|
||||||
export const globalPatches = new Set<GlobalContextMenuPatchCallback>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a context menu patch
|
|
||||||
* @param navId The navId(s) for the context menu(s) to patch
|
|
||||||
* @param patch The patch to be applied
|
|
||||||
*/
|
|
||||||
export function addContextMenuPatch(navId: string | Array<string>, patch: NavContextMenuPatchCallback) {
|
|
||||||
if (!Array.isArray(navId)) navId = [navId];
|
|
||||||
for (const id of navId) {
|
|
||||||
let contextMenuPatches = navPatches.get(id);
|
|
||||||
if (!contextMenuPatches) {
|
|
||||||
contextMenuPatches = new Set();
|
|
||||||
navPatches.set(id, contextMenuPatches);
|
|
||||||
}
|
|
||||||
|
|
||||||
contextMenuPatches.add(patch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a global context menu patch that fires the patch for all context menus
|
|
||||||
* @param patch The patch to be applied
|
|
||||||
*/
|
|
||||||
export function addGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallback) {
|
|
||||||
globalPatches.add(patch);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a context menu patch
|
|
||||||
* @param navId The navId(s) for the context menu(s) to remove the patch
|
|
||||||
* @param patch The patch to be removed
|
|
||||||
* @returns Wheter the patch was sucessfully removed from the context menu(s)
|
|
||||||
*/
|
|
||||||
export function removeContextMenuPatch<T extends string | Array<string>>(navId: T, patch: NavContextMenuPatchCallback): T extends string ? boolean : Array<boolean> {
|
|
||||||
const navIds = Array.isArray(navId) ? navId : [navId as string];
|
|
||||||
|
|
||||||
const results = navIds.map(id => navPatches.get(id)?.delete(patch) ?? false);
|
|
||||||
|
|
||||||
return (Array.isArray(navId) ? results : results[0]) as T extends string ? boolean : Array<boolean>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a global context menu patch
|
|
||||||
* @param patch The patch to be removed
|
|
||||||
* @returns Wheter the patch was sucessfully removed
|
|
||||||
*/
|
|
||||||
export function removeGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallback): boolean {
|
|
||||||
return globalPatches.delete(patch);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A helper function for finding the children array of a group nested inside a context menu based on the id of one of its childs
|
|
||||||
* @param id The id of the child
|
|
||||||
* @param children The context menu children
|
|
||||||
*/
|
|
||||||
export function findGroupChildrenByChildId(id: string, children: Array<React.ReactElement>, _itemsArray?: Array<React.ReactElement>): Array<React.ReactElement> | null {
|
|
||||||
for (const child of children) {
|
|
||||||
if (child == null) continue;
|
|
||||||
|
|
||||||
if (child.props?.id === id) return _itemsArray ?? null;
|
|
||||||
|
|
||||||
let nextChildren = child.props?.children;
|
|
||||||
if (nextChildren) {
|
|
||||||
if (!Array.isArray(nextChildren)) {
|
|
||||||
nextChildren = [nextChildren];
|
|
||||||
child.props.children = nextChildren;
|
|
||||||
}
|
|
||||||
|
|
||||||
const found = findGroupChildrenByChildId(id, nextChildren, nextChildren);
|
|
||||||
if (found !== null) return found;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ContextMenuProps {
|
|
||||||
contextMenuApiArguments?: Array<any>;
|
|
||||||
navId: string;
|
|
||||||
children: Array<ReactElement>;
|
|
||||||
"aria-label": string;
|
|
||||||
onSelect: (() => void) | undefined;
|
|
||||||
onClose: (callback: (...args: Array<any>) => any) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const patchedMenus = new WeakSet();
|
|
||||||
|
|
||||||
export function _patchContextMenu(props: ContextMenuProps) {
|
|
||||||
props.contextMenuApiArguments ??= [];
|
|
||||||
const contextMenuPatches = navPatches.get(props.navId);
|
|
||||||
|
|
||||||
if (!Array.isArray(props.children)) props.children = [props.children];
|
|
||||||
|
|
||||||
if (contextMenuPatches) {
|
|
||||||
for (const patch of contextMenuPatches) {
|
|
||||||
try {
|
|
||||||
const callback = patch(props.children, ...props.contextMenuApiArguments);
|
|
||||||
if (!patchedMenus.has(props)) callback?.();
|
|
||||||
} catch (err) {
|
|
||||||
ContextMenuLogger.error(`Patch for ${props.navId} errored,`, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const patch of globalPatches) {
|
|
||||||
try {
|
|
||||||
const callback = patch(props.navId, props.children, ...props.contextMenuApiArguments);
|
|
||||||
if (!patchedMenus.has(props)) callback?.();
|
|
||||||
} catch (err) {
|
|
||||||
ContextMenuLogger.error("Global patch errored,", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
patchedMenus.add(props);
|
|
||||||
}
|
|
@ -19,7 +19,6 @@
|
|||||||
import Logger from "@utils/Logger";
|
import Logger from "@utils/Logger";
|
||||||
import { MessageStore } from "@webpack/common";
|
import { MessageStore } from "@webpack/common";
|
||||||
import type { Channel, Message } from "discord-types/general";
|
import type { Channel, Message } from "discord-types/general";
|
||||||
import type { Promisable } from "type-fest";
|
|
||||||
|
|
||||||
const MessageEventsLogger = new Logger("MessageEvents", "#e5c890");
|
const MessageEventsLogger = new Logger("MessageEvents", "#e5c890");
|
||||||
|
|
||||||
@ -42,16 +41,16 @@ export interface MessageExtra {
|
|||||||
stickerIds?: string[];
|
stickerIds?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => Promisable<void | { cancel: boolean; }>;
|
export type SendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => void | { cancel: boolean; };
|
||||||
export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => Promisable<void>;
|
export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => void;
|
||||||
|
|
||||||
const sendListeners = new Set<SendListener>();
|
const sendListeners = new Set<SendListener>();
|
||||||
const editListeners = new Set<EditListener>();
|
const editListeners = new Set<EditListener>();
|
||||||
|
|
||||||
export async function _handlePreSend(channelId: string, messageObj: MessageObject, extra: MessageExtra) {
|
export function _handlePreSend(channelId: string, messageObj: MessageObject, extra: MessageExtra) {
|
||||||
for (const listener of sendListeners) {
|
for (const listener of sendListeners) {
|
||||||
try {
|
try {
|
||||||
const result = await listener(channelId, messageObj, extra);
|
const result = listener(channelId, messageObj, extra);
|
||||||
if (result && result.cancel === true) {
|
if (result && result.cancel === true) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -62,10 +61,10 @@ export async function _handlePreSend(channelId: string, messageObj: MessageObjec
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function _handlePreEdit(channelId: string, messageId: string, messageObj: MessageObject) {
|
export function _handlePreEdit(channelId: string, messageId: string, messageObj: MessageObject) {
|
||||||
for (const listener of editListeners) {
|
for (const listener of editListeners) {
|
||||||
try {
|
try {
|
||||||
await listener(channelId, messageId, messageObj);
|
listener(channelId, messageId, messageObj);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
MessageEventsLogger.error("MessageEditHandler: Listener encountered an unknown error\n", e);
|
MessageEventsLogger.error("MessageEditHandler: Listener encountered an unknown error\n", e);
|
||||||
}
|
}
|
||||||
|
@ -20,8 +20,7 @@ import "./styles.css";
|
|||||||
|
|
||||||
import { useSettings } from "@api/settings";
|
import { useSettings } from "@api/settings";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { classes } from "@utils/misc";
|
import { Forms, React, useEffect, useMemo, useState, useStateFromStores, WindowStore } from "@webpack/common";
|
||||||
import { React, useEffect, useMemo, useState, useStateFromStores, WindowStore } from "@webpack/common";
|
|
||||||
|
|
||||||
import { NotificationData } from "./Notifications";
|
import { NotificationData } from "./Notifications";
|
||||||
|
|
||||||
@ -33,11 +32,8 @@ export default ErrorBoundary.wrap(function NotificationComponent({
|
|||||||
icon,
|
icon,
|
||||||
onClick,
|
onClick,
|
||||||
onClose,
|
onClose,
|
||||||
image,
|
image
|
||||||
permanent,
|
}: NotificationData) {
|
||||||
className,
|
|
||||||
dismissOnClick
|
|
||||||
}: NotificationData & { className?: string; }) {
|
|
||||||
const { timeout, position } = useSettings(["notifications.timeout", "notifications.position"]).notifications;
|
const { timeout, position } = useSettings(["notifications.timeout", "notifications.position"]).notifications;
|
||||||
const hasFocus = useStateFromStores([WindowStore], () => WindowStore.isFocused());
|
const hasFocus = useStateFromStores([WindowStore], () => WindowStore.isFocused());
|
||||||
|
|
||||||
@ -47,7 +43,7 @@ export default ErrorBoundary.wrap(function NotificationComponent({
|
|||||||
const start = useMemo(() => Date.now(), [timeout, isHover, hasFocus]);
|
const start = useMemo(() => Date.now(), [timeout, isHover, hasFocus]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isHover || !hasFocus || timeout === 0 || permanent) return void setElapsed(0);
|
if (isHover || !hasFocus || timeout === 0) return void setElapsed(0);
|
||||||
|
|
||||||
const intervalId = setInterval(() => {
|
const intervalId = setInterval(() => {
|
||||||
const elapsed = Date.now() - start;
|
const elapsed = Date.now() - start;
|
||||||
@ -64,13 +60,9 @@ export default ErrorBoundary.wrap(function NotificationComponent({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={classes("vc-notification-root", className)}
|
className="vc-notification-root"
|
||||||
style={position === "bottom-right" ? { bottom: "1rem" } : { top: "3rem" }}
|
style={position === "bottom-right" ? { bottom: "1rem" } : { top: "3rem" }}
|
||||||
onClick={() => {
|
onClick={onClick}
|
||||||
onClick?.();
|
|
||||||
if (dismissOnClick !== false)
|
|
||||||
onClose!();
|
|
||||||
}}
|
|
||||||
onContextMenu={e => {
|
onContextMenu={e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@ -82,35 +74,14 @@ export default ErrorBoundary.wrap(function NotificationComponent({
|
|||||||
<div className="vc-notification">
|
<div className="vc-notification">
|
||||||
{icon && <img className="vc-notification-icon" src={icon} alt="" />}
|
{icon && <img className="vc-notification-icon" src={icon} alt="" />}
|
||||||
<div className="vc-notification-content">
|
<div className="vc-notification-content">
|
||||||
<div className="vc-notification-header">
|
<Forms.FormTitle tag="h2">{title}</Forms.FormTitle>
|
||||||
<h2 className="vc-notification-title">{title}</h2>
|
|
||||||
<button
|
|
||||||
className="vc-notification-close-btn"
|
|
||||||
onClick={e => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
onClose!();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
role="img"
|
|
||||||
aria-labelledby="vc-notification-dismiss-title"
|
|
||||||
>
|
|
||||||
<title id="vc-notification-dismiss-title">Dismiss Notification</title>
|
|
||||||
<path fill="currentColor" d="M18.4 4L12 10.4L5.6 4L4 5.6L10.4 12L4 18.4L5.6 20L12 13.6L18.4 20L20 18.4L13.6 12L20 5.6L18.4 4Z" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
{richBody ?? <p className="vc-notification-p">{body}</p>}
|
{richBody ?? <p className="vc-notification-p">{body}</p>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{image && <img className="vc-notification-img" src={image} alt="" />}
|
{image && <img className="vc-notification-img" src={image} alt="" />}
|
||||||
{timeout !== 0 && !permanent && (
|
{timeout !== 0 && (
|
||||||
<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-experiment)" }}
|
||||||
@ -118,6 +89,4 @@ export default ErrorBoundary.wrap(function NotificationComponent({
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}, {
|
|
||||||
onError: ({ props }) => props.onClose!()
|
|
||||||
});
|
});
|
||||||
|
@ -23,7 +23,6 @@ import type { ReactNode } from "react";
|
|||||||
import type { Root } from "react-dom/client";
|
import type { Root } from "react-dom/client";
|
||||||
|
|
||||||
import NotificationComponent from "./NotificationComponent";
|
import NotificationComponent from "./NotificationComponent";
|
||||||
import { persistNotification } from "./notificationLog";
|
|
||||||
|
|
||||||
const NotificationQueue = new Queue();
|
const NotificationQueue = new Queue();
|
||||||
|
|
||||||
@ -55,12 +54,6 @@ export interface NotificationData {
|
|||||||
onClick?(): void;
|
onClick?(): void;
|
||||||
onClose?(): void;
|
onClose?(): void;
|
||||||
color?: string;
|
color?: string;
|
||||||
/** Whether this notification should not have a timeout */
|
|
||||||
permanent?: boolean;
|
|
||||||
/** Whether this notification should not be persisted in the Notification Log */
|
|
||||||
noPersist?: boolean;
|
|
||||||
/** Whether this notification should be dismissed when clicked (defaults to true) */
|
|
||||||
dismissOnClick?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function _showNotification(notification: NotificationData, id: number) {
|
function _showNotification(notification: NotificationData, id: number) {
|
||||||
@ -77,8 +70,6 @@ function _showNotification(notification: NotificationData, id: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function shouldBeNative() {
|
function shouldBeNative() {
|
||||||
if (typeof Notification === "undefined") return false;
|
|
||||||
|
|
||||||
const { useNative } = Settings.notifications;
|
const { useNative } = Settings.notifications;
|
||||||
if (useNative === "always") return true;
|
if (useNative === "always") return true;
|
||||||
if (useNative === "not-focused") return !document.hasFocus();
|
if (useNative === "not-focused") return !document.hasFocus();
|
||||||
@ -93,8 +84,6 @@ export async function requestPermission() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function showNotification(data: NotificationData) {
|
export async function showNotification(data: NotificationData) {
|
||||||
persistNotification(data);
|
|
||||||
|
|
||||||
if (shouldBeNative() && await requestPermission()) {
|
if (shouldBeNative() && await requestPermission()) {
|
||||||
const { title, body, icon, image, onClick = null, onClose = null } = data;
|
const { title, body, icon, image, onClick = null, onClose = null } = data;
|
||||||
const n = new Notification(title, {
|
const n = new Notification(title, {
|
||||||
|
@ -1,203 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2023 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import * as DataStore from "@api/DataStore";
|
|
||||||
import { Settings } from "@api/settings";
|
|
||||||
import { classNameFactory } from "@api/Styles";
|
|
||||||
import { useAwaiter } from "@utils/misc";
|
|
||||||
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
|
||||||
import { Alerts, Button, Forms, moment, React, Text, Timestamp, useEffect, useReducer, useState } from "@webpack/common";
|
|
||||||
import { nanoid } from "nanoid";
|
|
||||||
import type { DispatchWithoutAction } from "react";
|
|
||||||
|
|
||||||
import NotificationComponent from "./NotificationComponent";
|
|
||||||
import type { NotificationData } from "./Notifications";
|
|
||||||
|
|
||||||
interface PersistentNotificationData extends Pick<NotificationData, "title" | "body" | "image" | "icon" | "color"> {
|
|
||||||
timestamp: number;
|
|
||||||
id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const KEY = "notification-log";
|
|
||||||
|
|
||||||
const getLog = async () => {
|
|
||||||
const log = await DataStore.get(KEY) as PersistentNotificationData[] | undefined;
|
|
||||||
return log ?? [];
|
|
||||||
};
|
|
||||||
|
|
||||||
const cl = classNameFactory("vc-notification-log-");
|
|
||||||
const signals = new Set<DispatchWithoutAction>();
|
|
||||||
|
|
||||||
export async function persistNotification(notification: NotificationData) {
|
|
||||||
if (notification.noPersist) return;
|
|
||||||
|
|
||||||
const limit = Settings.notifications.logLimit;
|
|
||||||
if (limit === 0) return;
|
|
||||||
|
|
||||||
await DataStore.update(KEY, (old: PersistentNotificationData[] | undefined) => {
|
|
||||||
const log = old ?? [];
|
|
||||||
|
|
||||||
// Omit stuff we don't need
|
|
||||||
const {
|
|
||||||
onClick, onClose, richBody, permanent, noPersist, dismissOnClick,
|
|
||||||
...pureNotification
|
|
||||||
} = notification;
|
|
||||||
|
|
||||||
log.unshift({
|
|
||||||
...pureNotification,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
id: nanoid()
|
|
||||||
});
|
|
||||||
|
|
||||||
if (log.length > limit && limit !== 200)
|
|
||||||
log.length = limit;
|
|
||||||
|
|
||||||
return log;
|
|
||||||
});
|
|
||||||
|
|
||||||
signals.forEach(x => x());
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteNotification(timestamp: number) {
|
|
||||||
const log = await getLog();
|
|
||||||
const index = log.findIndex(x => x.timestamp === timestamp);
|
|
||||||
if (index === -1) return;
|
|
||||||
|
|
||||||
log.splice(index, 1);
|
|
||||||
await DataStore.set(KEY, log);
|
|
||||||
signals.forEach(x => x());
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useLogs() {
|
|
||||||
const [signal, setSignal] = useReducer(x => x + 1, 0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
signals.add(setSignal);
|
|
||||||
return () => void signals.delete(setSignal);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const [log, _, pending] = useAwaiter(getLog, {
|
|
||||||
fallbackValue: [],
|
|
||||||
deps: [signal]
|
|
||||||
});
|
|
||||||
|
|
||||||
return [log, pending] as const;
|
|
||||||
}
|
|
||||||
|
|
||||||
function NotificationEntry({ data }: { data: PersistentNotificationData; }) {
|
|
||||||
const [removing, setRemoving] = useState(false);
|
|
||||||
const ref = React.useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const div = ref.current!;
|
|
||||||
|
|
||||||
const setHeight = () => {
|
|
||||||
if (div.clientHeight === 0) return requestAnimationFrame(setHeight);
|
|
||||||
div.style.height = `${div.clientHeight}px`;
|
|
||||||
};
|
|
||||||
|
|
||||||
setHeight();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cl("wrapper", { removing })} ref={ref}>
|
|
||||||
<NotificationComponent
|
|
||||||
{...data}
|
|
||||||
permanent={true}
|
|
||||||
dismissOnClick={false}
|
|
||||||
onClose={() => {
|
|
||||||
if (removing) return;
|
|
||||||
setRemoving(true);
|
|
||||||
|
|
||||||
setTimeout(() => deleteNotification(data.timestamp), 200);
|
|
||||||
}}
|
|
||||||
richBody={
|
|
||||||
<div className={cl("body")}>
|
|
||||||
{data.body}
|
|
||||||
<Timestamp timestamp={moment(data.timestamp)} className={cl("timestamp")} />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function NotificationLog({ log, pending }: { log: PersistentNotificationData[], pending: boolean; }) {
|
|
||||||
if (!log.length && !pending)
|
|
||||||
return (
|
|
||||||
<div className={cl("container")}>
|
|
||||||
<div className={cl("empty")} />
|
|
||||||
<Forms.FormText style={{ textAlign: "center" }}>
|
|
||||||
No notifications yet
|
|
||||||
</Forms.FormText>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cl("container")}>
|
|
||||||
{log.map(n => <NotificationEntry data={n} key={n.id} />)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function LogModal({ modalProps, close }: { modalProps: ModalProps; close(): void; }) {
|
|
||||||
const [log, pending] = useLogs();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalRoot {...modalProps} size={ModalSize.LARGE}>
|
|
||||||
<ModalHeader>
|
|
||||||
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>Notification Log</Text>
|
|
||||||
<ModalCloseButton onClick={close} />
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalContent>
|
|
||||||
<NotificationLog log={log} pending={pending} />
|
|
||||||
</ModalContent>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
<Button
|
|
||||||
disabled={log.length === 0}
|
|
||||||
onClick={() => {
|
|
||||||
Alerts.show({
|
|
||||||
title: "Are you sure?",
|
|
||||||
body: `This will permanently remove ${log.length} notification${log.length === 1 ? "" : "s"}. This action cannot be undone.`,
|
|
||||||
async onConfirm() {
|
|
||||||
await DataStore.set(KEY, []);
|
|
||||||
signals.forEach(x => x());
|
|
||||||
},
|
|
||||||
confirmText: "Do it!",
|
|
||||||
confirmColor: "vc-notification-log-danger-btn",
|
|
||||||
cancelText: "Nevermind"
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Clear Notification Log
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalRoot>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function openNotificationLogModal() {
|
|
||||||
const key = openModal(modalProps => (
|
|
||||||
<LogModal
|
|
||||||
modalProps={modalProps}
|
|
||||||
close={() => closeModal(key)}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
}
|
|
@ -3,20 +3,16 @@
|
|||||||
all: unset;
|
all: unset;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
width: 25vw;
|
||||||
|
min-height: 10vh;
|
||||||
color: var(--text-normal);
|
color: var(--text-normal);
|
||||||
background-color: var(--background-secondary-alt);
|
background-color: var(--background-secondary-alt);
|
||||||
border-radius: 6px;
|
|
||||||
overflow: hidden;
|
|
||||||
cursor: pointer;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-root:not(.vc-notification-log-wrapper > .vc-notification-root) {
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 2147483647;
|
z-index: 2147483647;
|
||||||
right: 1rem;
|
right: 1rem;
|
||||||
width: 25vw;
|
border-radius: 6px;
|
||||||
min-height: 10vh;
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-notification {
|
.vc-notification {
|
||||||
@ -26,42 +22,17 @@
|
|||||||
gap: 1.25rem;
|
gap: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-notification-content {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-title {
|
|
||||||
color: var(--header-primary);
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.25rem;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-close-btn {
|
|
||||||
all: unset;
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--interactive-normal);
|
|
||||||
opacity: 0.5;
|
|
||||||
transition: opacity 0.2s ease-in-out, color 0.2s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-close-btn:hover {
|
|
||||||
color: var(--interactive-hover);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-icon {
|
.vc-notification-icon {
|
||||||
height: 4rem;
|
height: 4rem;
|
||||||
width: 4rem;
|
width: 4rem;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Discord adding 3km margin to generic tags */
|
||||||
|
.vc-notification h2 {
|
||||||
|
margin: unset;
|
||||||
|
}
|
||||||
|
|
||||||
.vc-notification-progressbar {
|
.vc-notification-progressbar {
|
||||||
height: 0.25rem;
|
height: 0.25rem;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
@ -76,47 +47,3 @@
|
|||||||
.vc-notification-img {
|
.vc-notification-img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-notification-log-empty {
|
|
||||||
height: 218px;
|
|
||||||
background: url("/assets/b36de980b174d7b798c89f35c116e5c6.svg") center no-repeat;
|
|
||||||
margin-bottom: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-log-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 1em;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-log-wrapper {
|
|
||||||
transition: 200ms ease;
|
|
||||||
transition-property: height, opacity;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-log-wrapper:not(:last-child) {
|
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-log-removing {
|
|
||||||
height: 0 !important;
|
|
||||||
opacity: 0;
|
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-log-body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-log-timestamp {
|
|
||||||
margin-left: auto;
|
|
||||||
font-size: 0.8em;
|
|
||||||
font-weight: lighter;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-log-danger-btn {
|
|
||||||
color: var(--white-500);
|
|
||||||
background-color: var(--button-danger-background);
|
|
||||||
}
|
|
||||||
|
@ -1,69 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2023 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import Logger from "@utils/Logger";
|
|
||||||
import { proxyLazy } from "@utils/proxyLazy";
|
|
||||||
import { findModuleId, wreq } from "@webpack";
|
|
||||||
|
|
||||||
import { Settings } from "./settings";
|
|
||||||
|
|
||||||
interface Setting<T> {
|
|
||||||
/**
|
|
||||||
* Get the setting value
|
|
||||||
*/
|
|
||||||
getSetting(): T;
|
|
||||||
/**
|
|
||||||
* Update the setting value
|
|
||||||
* @param value The new value
|
|
||||||
*/
|
|
||||||
updateSetting(value: T | ((old: T) => T)): Promise<void>;
|
|
||||||
/**
|
|
||||||
* React hook for automatically updating components when the setting is updated
|
|
||||||
*/
|
|
||||||
useSetting(): T;
|
|
||||||
settingsStoreApiGroup: string;
|
|
||||||
settingsStoreApiName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SettingsStores: Array<Setting<any>> | undefined = proxyLazy(() => {
|
|
||||||
const modId = findModuleId('"textAndImages","renderSpoilers"');
|
|
||||||
if (modId == null) return new Logger("SettingsStoreAPI").error("Didn't find stores module.");
|
|
||||||
|
|
||||||
const mod = wreq(modId);
|
|
||||||
if (mod == null) return;
|
|
||||||
|
|
||||||
return Object.values(mod).filter((s: any) => s?.settingsStoreApiGroup) as any;
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the store for a setting
|
|
||||||
* @param group The setting group
|
|
||||||
* @param name The name of the setting
|
|
||||||
*/
|
|
||||||
export function getSettingStore<T = any>(group: string, name: string): Setting<T> | undefined {
|
|
||||||
if (!Settings.plugins.SettingsStoreAPI.enabled) throw new Error("Cannot use SettingsStoreAPI without setting as dependency.");
|
|
||||||
|
|
||||||
return SettingsStores?.find(s => s?.settingsStoreApiGroup === group && s?.settingsStoreApiName === name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* getSettingStore but lazy
|
|
||||||
*/
|
|
||||||
export function getSettingStoreLazy<T = any>(group: string, name: string) {
|
|
||||||
return proxyLazy(() => getSettingStore<T>(group, name));
|
|
||||||
}
|
|
@ -18,7 +18,6 @@
|
|||||||
|
|
||||||
import * as $Badges from "./Badges";
|
import * as $Badges from "./Badges";
|
||||||
import * as $Commands from "./Commands";
|
import * as $Commands from "./Commands";
|
||||||
import * as $ContextMenu from "./ContextMenu";
|
|
||||||
import * as $DataStore from "./DataStore";
|
import * as $DataStore from "./DataStore";
|
||||||
import * as $MemberListDecorators from "./MemberListDecorators";
|
import * as $MemberListDecorators from "./MemberListDecorators";
|
||||||
import * as $MessageAccessories from "./MessageAccessories";
|
import * as $MessageAccessories from "./MessageAccessories";
|
||||||
@ -28,7 +27,6 @@ import * as $MessagePopover from "./MessagePopover";
|
|||||||
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 $SettingsStore from "./SettingsStore";
|
|
||||||
import * as $Styles from "./Styles";
|
import * as $Styles from "./Styles";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -86,10 +84,6 @@ export const MessageDecorations = $MessageDecorations;
|
|||||||
* An API allowing you to add components to member list users, in both DM's and servers
|
* An API allowing you to add components to member list users, in both DM's and servers
|
||||||
*/
|
*/
|
||||||
export const MemberListDecorators = $MemberListDecorators;
|
export const MemberListDecorators = $MemberListDecorators;
|
||||||
/**
|
|
||||||
* An API allowing you to read, manipulate and automatically update components based on Discord settings
|
|
||||||
*/
|
|
||||||
export const SettingsStore = $SettingsStore;
|
|
||||||
/**
|
/**
|
||||||
* An API allowing you to dynamically load styles
|
* An API allowing you to dynamically load styles
|
||||||
* a
|
* a
|
||||||
@ -99,8 +93,3 @@ export const Styles = $Styles;
|
|||||||
* An API allowing you to display notifications
|
* An API allowing you to display notifications
|
||||||
*/
|
*/
|
||||||
export const Notifications = $Notifications;
|
export const Notifications = $Notifications;
|
||||||
|
|
||||||
/**
|
|
||||||
* An api allowing you to patch and add/remove items to/from context menus
|
|
||||||
*/
|
|
||||||
export const ContextMenu = $ContextMenu;
|
|
||||||
|
@ -16,12 +16,9 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { debounce } from "@utils/debounce";
|
|
||||||
import IpcEvents from "@utils/IpcEvents";
|
import IpcEvents from "@utils/IpcEvents";
|
||||||
import { localStorage } from "@utils/localStorage";
|
|
||||||
import Logger from "@utils/Logger";
|
import Logger from "@utils/Logger";
|
||||||
import { mergeDefaults } from "@utils/misc";
|
import { mergeDefaults } from "@utils/misc";
|
||||||
import { putCloudSettings } from "@utils/settingsSync";
|
|
||||||
import { DefinedSettings, OptionType, SettingsChecks, SettingsDefinition } from "@utils/types";
|
import { DefinedSettings, OptionType, SettingsChecks, SettingsDefinition } from "@utils/types";
|
||||||
import { React } from "@webpack/common";
|
import { React } from "@webpack/common";
|
||||||
|
|
||||||
@ -31,16 +28,12 @@ const logger = new Logger("Settings");
|
|||||||
export interface Settings {
|
export interface Settings {
|
||||||
notifyAboutUpdates: boolean;
|
notifyAboutUpdates: boolean;
|
||||||
autoUpdate: boolean;
|
autoUpdate: boolean;
|
||||||
autoUpdateNotification: boolean,
|
|
||||||
useQuickCss: boolean;
|
useQuickCss: boolean;
|
||||||
enableReactDevtools: boolean;
|
enableReactDevtools: boolean;
|
||||||
themeLinks: string[];
|
themeLinks: string[];
|
||||||
frameless: boolean;
|
frameless: boolean;
|
||||||
transparent: boolean;
|
transparent: boolean;
|
||||||
winCtrlQ: boolean;
|
winCtrlQ: boolean;
|
||||||
macosTranslucency: boolean;
|
|
||||||
disableMinSize: boolean;
|
|
||||||
winNativeTitleBar: boolean;
|
|
||||||
plugins: {
|
plugins: {
|
||||||
[plugin: string]: {
|
[plugin: string]: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@ -52,44 +45,24 @@ export interface Settings {
|
|||||||
timeout: number;
|
timeout: number;
|
||||||
position: "top-right" | "bottom-right";
|
position: "top-right" | "bottom-right";
|
||||||
useNative: "always" | "never" | "not-focused";
|
useNative: "always" | "never" | "not-focused";
|
||||||
logLimit: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
cloud: {
|
|
||||||
authenticated: boolean;
|
|
||||||
url: string;
|
|
||||||
settingsSync: boolean;
|
|
||||||
settingsSyncVersion: number;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultSettings: Settings = {
|
const DefaultSettings: Settings = {
|
||||||
notifyAboutUpdates: true,
|
notifyAboutUpdates: true,
|
||||||
autoUpdate: false,
|
autoUpdate: false,
|
||||||
autoUpdateNotification: true,
|
|
||||||
useQuickCss: true,
|
useQuickCss: true,
|
||||||
themeLinks: [],
|
themeLinks: [],
|
||||||
enableReactDevtools: false,
|
enableReactDevtools: false,
|
||||||
frameless: false,
|
frameless: false,
|
||||||
transparent: false,
|
transparent: false,
|
||||||
winCtrlQ: false,
|
winCtrlQ: false,
|
||||||
macosTranslucency: false,
|
|
||||||
disableMinSize: false,
|
|
||||||
winNativeTitleBar: false,
|
|
||||||
plugins: {},
|
plugins: {},
|
||||||
|
|
||||||
notifications: {
|
notifications: {
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
position: "bottom-right",
|
position: "bottom-right",
|
||||||
useNative: "not-focused",
|
useNative: "not-focused"
|
||||||
logLimit: 50
|
|
||||||
},
|
|
||||||
|
|
||||||
cloud: {
|
|
||||||
authenticated: false,
|
|
||||||
url: "https://api.vencord.dev/",
|
|
||||||
settingsSync: false,
|
|
||||||
settingsSyncVersion: 0
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -101,13 +74,6 @@ try {
|
|||||||
logger.error("An error occurred while loading the settings. Corrupt settings file?\n", err);
|
logger.error("An error occurred while loading the settings. Corrupt settings file?\n", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveSettingsOnFrequentAction = debounce(async () => {
|
|
||||||
if (Settings.cloud.settingsSync && Settings.cloud.authenticated) {
|
|
||||||
await putCloudSettings();
|
|
||||||
delete localStorage.Vencord_settingsDirty;
|
|
||||||
}
|
|
||||||
}, 60_000);
|
|
||||||
|
|
||||||
type SubscriptionCallback = ((newValue: any, path: string) => void) & { _path?: string; };
|
type SubscriptionCallback = ((newValue: any, path: string) => void) & { _path?: string; };
|
||||||
const subscriptions = new Set<SubscriptionCallback>();
|
const subscriptions = new Set<SubscriptionCallback>();
|
||||||
|
|
||||||
@ -124,7 +90,7 @@ function makeProxy(settings: any, root = settings, path = ""): Settings {
|
|||||||
// Return empty for plugins with no settings
|
// Return empty for plugins with no settings
|
||||||
if (path === "plugins" && p in plugins)
|
if (path === "plugins" && p in plugins)
|
||||||
return target[p] = makeProxy({
|
return target[p] = makeProxy({
|
||||||
enabled: plugins[p].required ?? plugins[p].enabledByDefault ?? false
|
enabled: plugins[p].required ?? false
|
||||||
}, root, `plugins.${p}`);
|
}, root, `plugins.${p}`);
|
||||||
|
|
||||||
// Since the property is not set, check if this is a plugin's setting and if so, try to resolve
|
// Since the property is not set, check if this is a plugin's setting and if so, try to resolve
|
||||||
@ -163,16 +129,12 @@ function makeProxy(settings: any, root = settings, path = ""): Settings {
|
|||||||
target[p] = v;
|
target[p] = v;
|
||||||
// Call any listeners that are listening to a setting of this path
|
// Call any listeners that are listening to a setting of this path
|
||||||
const setPath = `${path}${path && "."}${p}`;
|
const setPath = `${path}${path && "."}${p}`;
|
||||||
delete proxyCache[setPath];
|
|
||||||
for (const subscription of subscriptions) {
|
for (const subscription of subscriptions) {
|
||||||
if (!subscription._path || subscription._path === setPath) {
|
if (!subscription._path || subscription._path === setPath) {
|
||||||
subscription(v, setPath);
|
subscription(v, setPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// And don't forget to persist the settings!
|
// And don't forget to persist the settings!
|
||||||
PlainSettings.cloud.settingsSyncVersion = Date.now();
|
|
||||||
localStorage.Vencord_settingsDirty = true;
|
|
||||||
saveSettingsOnFrequentAction();
|
|
||||||
VencordNative.ipc.invoke(IpcEvents.SET_SETTINGS, JSON.stringify(root, null, 4));
|
VencordNative.ipc.invoke(IpcEvents.SET_SETTINGS, JSON.stringify(root, null, 4));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -203,11 +165,11 @@ export const Settings = makeProxy(settings);
|
|||||||
* @returns Settings
|
* @returns Settings
|
||||||
*/
|
*/
|
||||||
// TODO: Representing paths as essentially "string[].join('.')" wont allow dots in paths, change to "paths?: string[][]" later
|
// TODO: Representing paths as essentially "string[].join('.')" wont allow dots in paths, change to "paths?: string[][]" later
|
||||||
export function useSettings(paths?: UseSettings<Settings>[]) {
|
export function useSettings(paths?: string[]) {
|
||||||
const [, forceUpdate] = React.useReducer(() => ({}), {});
|
const [, forceUpdate] = React.useReducer(() => ({}), {});
|
||||||
|
|
||||||
const onUpdate: SubscriptionCallback = paths
|
const onUpdate: SubscriptionCallback = paths
|
||||||
? (value, path) => paths.includes(path as UseSettings<Settings>) && forceUpdate()
|
? (value, path) => paths.includes(path) && forceUpdate()
|
||||||
: forceUpdate;
|
: forceUpdate;
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@ -265,7 +227,7 @@ export function definePluginSettings<D extends SettingsDefinition, C extends Set
|
|||||||
return Settings.plugins[definedSettings.pluginName] as any;
|
return Settings.plugins[definedSettings.pluginName] as any;
|
||||||
},
|
},
|
||||||
use: settings => useSettings(
|
use: settings => useSettings(
|
||||||
settings?.map(name => `plugins.${definedSettings.pluginName}.${name}`) as UseSettings<Settings>[]
|
settings?.map(name => `plugins.${definedSettings.pluginName}.${name}`)
|
||||||
).plugins[definedSettings.pluginName] as any,
|
).plugins[definedSettings.pluginName] as any,
|
||||||
def,
|
def,
|
||||||
checks: checks ?? {},
|
checks: checks ?? {},
|
||||||
@ -273,15 +235,3 @@ export function definePluginSettings<D extends SettingsDefinition, C extends Set
|
|||||||
};
|
};
|
||||||
return definedSettings;
|
return definedSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
type UseSettings<T extends object> = ResolveUseSettings<T>[keyof T];
|
|
||||||
|
|
||||||
type ResolveUseSettings<T extends object> = {
|
|
||||||
[Key in keyof T]:
|
|
||||||
Key extends string
|
|
||||||
? T[Key] extends Record<string, unknown>
|
|
||||||
// @ts-ignore "Type instantiation is excessively deep and possibly infinite"
|
|
||||||
? UseSettings<T[Key]> extends string ? `${Key}.${UseSettings<T[Key]>}` : never
|
|
||||||
: Key
|
|
||||||
: never;
|
|
||||||
};
|
|
||||||
|
@ -17,24 +17,20 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Logger from "@utils/Logger";
|
import Logger from "@utils/Logger";
|
||||||
import { Margins } from "@utils/margins";
|
|
||||||
import { LazyComponent } from "@utils/misc";
|
import { LazyComponent } from "@utils/misc";
|
||||||
import { React } from "@webpack/common";
|
import { Margins, React } from "@webpack/common";
|
||||||
|
|
||||||
import { ErrorCard } from "./ErrorCard";
|
import { ErrorCard } from "./ErrorCard";
|
||||||
|
|
||||||
interface Props<T = any> {
|
interface Props {
|
||||||
/** Render nothing if an error occurs */
|
/** Render nothing if an error occurs */
|
||||||
noop?: boolean;
|
noop?: boolean;
|
||||||
/** Fallback component to render if an error occurs */
|
/** Fallback component to render if an error occurs */
|
||||||
fallback?: React.ComponentType<React.PropsWithChildren<{ error: any; message: string; stack: string; }>>;
|
fallback?: React.ComponentType<React.PropsWithChildren<{ error: any; message: string; stack: string; }>>;
|
||||||
/** called when an error occurs. The props property is only available if using .wrap */
|
/** called when an error occurs */
|
||||||
onError?(data: { error: Error, errorInfo: React.ErrorInfo, props: T; }): void;
|
onError?(error: Error, errorInfo: React.ErrorInfo): void;
|
||||||
/** Custom error message */
|
/** Custom error message */
|
||||||
message?: string;
|
message?: string;
|
||||||
|
|
||||||
/** The props passed to the wrapped component. Only used by wrap */
|
|
||||||
wrappedProps?: T;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const color = "#e78284";
|
const color = "#e78284";
|
||||||
@ -69,7 +65,7 @@ const ErrorBoundary = LazyComponent(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||||
this.props.onError?.({ error, errorInfo, props: this.props.wrappedProps });
|
this.props.onError?.(error, errorInfo);
|
||||||
logger.error("A component threw an Error\n", error);
|
logger.error("A component threw an Error\n", error);
|
||||||
logger.error("Component Stack", errorInfo.componentStack);
|
logger.error("Component Stack", errorInfo.componentStack);
|
||||||
}
|
}
|
||||||
@ -88,13 +84,15 @@ const ErrorBoundary = LazyComponent(() => {
|
|||||||
const msg = this.props.message || "An error occurred while rendering this Component. More info can be found below and in your console.";
|
const msg = this.props.message || "An error occurred while rendering this Component. More info can be found below and in your console.";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorCard style={{ overflow: "hidden" }}>
|
<ErrorCard style={{
|
||||||
|
overflow: "hidden",
|
||||||
|
}}>
|
||||||
<h1>Oh no!</h1>
|
<h1>Oh no!</h1>
|
||||||
<p>{msg}</p>
|
<p>{msg}</p>
|
||||||
<code>
|
<code>
|
||||||
{this.state.message}
|
{this.state.message}
|
||||||
{!!this.state.stack && (
|
{!!this.state.stack && (
|
||||||
<pre className={Margins.top8}>
|
<pre className={Margins.marginTop8}>
|
||||||
{this.state.stack}
|
{this.state.stack}
|
||||||
</pre>
|
</pre>
|
||||||
)}
|
)}
|
||||||
@ -105,11 +103,11 @@ const ErrorBoundary = LazyComponent(() => {
|
|||||||
};
|
};
|
||||||
}) as
|
}) as
|
||||||
React.ComponentType<React.PropsWithChildren<Props>> & {
|
React.ComponentType<React.PropsWithChildren<Props>> & {
|
||||||
wrap<T extends object = any>(Component: React.ComponentType<T>, errorBoundaryProps?: Omit<Props<T>, "wrappedProps">): React.FunctionComponent<T>;
|
wrap<T extends object = any>(Component: React.ComponentType<T>, errorBoundaryProps?: Props): React.ComponentType<T>;
|
||||||
};
|
};
|
||||||
|
|
||||||
ErrorBoundary.wrap = (Component, errorBoundaryProps) => props => (
|
ErrorBoundary.wrap = (Component, errorBoundaryProps) => props => (
|
||||||
<ErrorBoundary {...errorBoundaryProps} wrappedProps={props}>
|
<ErrorBoundary {...errorBoundaryProps}>
|
||||||
<Component {...props} />
|
<Component {...props} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
.vc-error-card {
|
|
||||||
padding: 2em;
|
|
||||||
background-color: #e7828430;
|
|
||||||
border: 1px solid #e78284;
|
|
||||||
border-radius: 5px;
|
|
||||||
color: var(--text-normal, white);
|
|
||||||
}
|
|
@ -16,15 +16,24 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import "./ErrorCard.css";
|
import { Card } from "@webpack/common";
|
||||||
|
|
||||||
import { classes } from "@utils/misc";
|
interface Props {
|
||||||
import type { HTMLProps } from "react";
|
style?: React.CSSProperties;
|
||||||
|
className?: string;
|
||||||
export function ErrorCard(props: React.PropsWithChildren<HTMLProps<HTMLDivElement>>) {
|
}
|
||||||
|
export function ErrorCard(props: React.PropsWithChildren<Props>) {
|
||||||
return (
|
return (
|
||||||
<div {...props} className={classes(props.className, "vc-error-card")}>
|
<Card className={props.className} style={
|
||||||
|
{
|
||||||
|
padding: "2em",
|
||||||
|
backgroundColor: "#e7828430",
|
||||||
|
borderColor: "#e78284",
|
||||||
|
color: "var(--text-normal)",
|
||||||
|
...props.style
|
||||||
|
}
|
||||||
|
}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -17,12 +17,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { debounce } from "@utils/debounce";
|
import { debounce } from "@utils/debounce";
|
||||||
import { Margins } from "@utils/margins";
|
|
||||||
import { makeCodeblock } from "@utils/misc";
|
import { makeCodeblock } from "@utils/misc";
|
||||||
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
|
import { canonicalizeMatch, canonicalizeReplace, ReplaceFn } from "@utils/patches";
|
||||||
import { ReplaceFn } from "@utils/types";
|
|
||||||
import { search } from "@webpack";
|
import { search } from "@webpack";
|
||||||
import { Button, Clipboard, Forms, Parser, React, Switch, Text, TextInput } from "@webpack/common";
|
import { Button, Clipboard, Forms, Margins, Parser, React, Switch, Text, TextInput } from "@webpack/common";
|
||||||
|
|
||||||
import { CheckedTextInput } from "./CheckedTextInput";
|
import { CheckedTextInput } from "./CheckedTextInput";
|
||||||
import ErrorBoundary from "./ErrorBoundary";
|
import ErrorBoundary from "./ErrorBoundary";
|
||||||
@ -130,7 +128,7 @@ function ReplacementComponent({ module, match, replacement, setReplacementError
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!!diff?.length && (
|
{!!diff?.length && (
|
||||||
<Button className={Margins.top20} onClick={() => {
|
<Button className={Margins.marginTop20} onClick={() => {
|
||||||
try {
|
try {
|
||||||
Function(patchedCode.replace(/^function\(/, "function patchedModule("));
|
Function(patchedCode.replace(/^function\(/, "function patchedModule("));
|
||||||
setCompileResult([true, "Compiled successfully"]);
|
setCompileResult([true, "Compiled successfully"]);
|
||||||
@ -186,10 +184,9 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
|
|||||||
error={error ?? replacementError}
|
error={error ?? replacementError}
|
||||||
/>
|
/>
|
||||||
{!isFunc && (
|
{!isFunc && (
|
||||||
<div className="vc-text-selectable">
|
<>
|
||||||
<Forms.FormTitle>Cheat Sheet</Forms.FormTitle>
|
<Forms.FormTitle>Cheat Sheet</Forms.FormTitle>
|
||||||
{Object.entries({
|
{Object.entries({
|
||||||
"\\i": "Special regex escape sequence that matches identifiers (varnames, classnames, etc.)",
|
|
||||||
"$$": "Insert a $",
|
"$$": "Insert a $",
|
||||||
"$&": "Insert the entire match",
|
"$&": "Insert the entire match",
|
||||||
"$`\u200b": "Insert the substring before the match",
|
"$`\u200b": "Insert the substring before the match",
|
||||||
@ -201,11 +198,11 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
|
|||||||
{Parser.parse("`" + placeholder + "`")}: {desc}
|
{Parser.parse("`" + placeholder + "`")}: {desc}
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
))}
|
))}
|
||||||
</div>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Switch
|
<Switch
|
||||||
className={Margins.top8}
|
className={Margins.marginTop8}
|
||||||
value={isFunc}
|
value={isFunc}
|
||||||
onChange={setIsFunc}
|
onChange={setIsFunc}
|
||||||
note="'replacement' will be evaled if this is toggled"
|
note="'replacement' will be evaled if this is toggled"
|
||||||
@ -259,7 +256,7 @@ function PatchHelper() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Forms.FormSection>
|
<Forms.FormSection>
|
||||||
<Text variant="heading-md/normal" tag="h2" className={Margins.bottom8}>Patch Helper</Text>
|
<Text variant="heading-md/normal" tag="h2" className={Margins.marginBottom8}>Patch Helper</Text>
|
||||||
<Forms.FormTitle>find</Forms.FormTitle>
|
<Forms.FormTitle>find</Forms.FormTitle>
|
||||||
<TextInput
|
<TextInput
|
||||||
type="text"
|
type="text"
|
||||||
@ -299,7 +296,7 @@ function PatchHelper() {
|
|||||||
|
|
||||||
{!!(find && match && replacement) && (
|
{!!(find && match && replacement) && (
|
||||||
<>
|
<>
|
||||||
<Forms.FormTitle className={Margins.top20}>Code</Forms.FormTitle>
|
<Forms.FormTitle className={Margins.marginTop20}>Code</Forms.FormTitle>
|
||||||
<div style={{ userSelect: "text" }}>{Parser.parse(makeCodeblock(code, "ts"))}</div>
|
<div style={{ userSelect: "text" }}>{Parser.parse(makeCodeblock(code, "ts"))}</div>
|
||||||
<Button onClick={() => Clipboard.copy(code)}>Copy to Clipboard</Button>
|
<Button onClick={() => Clipboard.copy(code)}>Copy to Clipboard</Button>
|
||||||
</>
|
</>
|
||||||
|
@ -20,8 +20,7 @@ import { generateId } from "@api/Commands";
|
|||||||
import { useSettings } from "@api/settings";
|
import { useSettings } from "@api/settings";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Flex } from "@components/Flex";
|
import { Flex } from "@components/Flex";
|
||||||
import { Margins } from "@utils/margins";
|
import { LazyComponent } from "@utils/misc";
|
||||||
import { classes, LazyComponent } from "@utils/misc";
|
|
||||||
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
|
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
|
||||||
import { proxyLazy } from "@utils/proxyLazy";
|
import { proxyLazy } from "@utils/proxyLazy";
|
||||||
import { OptionType, Plugin } from "@utils/types";
|
import { OptionType, Plugin } from "@utils/types";
|
||||||
@ -175,7 +174,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalRoot transitionState={transitionState} size={ModalSize.MEDIUM} className="vc-text-selectable">
|
<ModalRoot transitionState={transitionState} size={ModalSize.MEDIUM}>
|
||||||
<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>
|
||||||
<ModalCloseButton onClick={onClose} />
|
<ModalCloseButton onClick={onClose} />
|
||||||
@ -199,7 +198,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||||||
</div>
|
</div>
|
||||||
</Forms.FormSection>
|
</Forms.FormSection>
|
||||||
{!!plugin.settingsAboutComponent && (
|
{!!plugin.settingsAboutComponent && (
|
||||||
<div className={classes(Margins.bottom8, "vc-text-selectable")}>
|
<div style={{ marginBottom: 8 }}>
|
||||||
<Forms.FormSection>
|
<Forms.FormSection>
|
||||||
<ErrorBoundary message="An error occurred while rendering this plugin's custom InfoComponent">
|
<ErrorBoundary message="An error occurred while rendering this plugin's custom InfoComponent">
|
||||||
<plugin.settingsAboutComponent tempSettings={tempSettings} />
|
<plugin.settingsAboutComponent tempSettings={tempSettings} />
|
||||||
|
@ -38,12 +38,9 @@ export function SettingNumericComponent({ option, pluginSettings, definedSetting
|
|||||||
|
|
||||||
function handleChange(newValue) {
|
function handleChange(newValue) {
|
||||||
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
||||||
|
|
||||||
setError(null);
|
|
||||||
if (typeof isValid === "string") setError(isValid);
|
if (typeof isValid === "string") setError(isValid);
|
||||||
else if (!isValid) setError("Invalid input provided.");
|
else if (!isValid) setError("Invalid input provided.");
|
||||||
|
else if (option.type === OptionType.NUMBER && BigInt(newValue) >= MAX_SAFE_NUMBER) {
|
||||||
if (option.type === OptionType.NUMBER && BigInt(newValue) >= MAX_SAFE_NUMBER) {
|
|
||||||
setState(`${Number.MAX_SAFE_INTEGER}`);
|
setState(`${Number.MAX_SAFE_INTEGER}`);
|
||||||
onChange(serialize(newValue));
|
onChange(serialize(newValue));
|
||||||
} else {
|
} else {
|
||||||
|
@ -36,7 +36,6 @@ export function SettingSelectComponent({ option, pluginSettings, definedSettings
|
|||||||
if (typeof isValid === "string") setError(isValid);
|
if (typeof isValid === "string") setError(isValid);
|
||||||
else if (!isValid) setError("Invalid input provided.");
|
else if (!isValid) setError("Invalid input provided.");
|
||||||
else {
|
else {
|
||||||
setError(null);
|
|
||||||
setState(newValue);
|
setState(newValue);
|
||||||
onChange(newValue);
|
onChange(newValue);
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,6 @@ export function SettingTextComponent({ option, pluginSettings, definedSettings,
|
|||||||
if (typeof isValid === "string") setError(isValid);
|
if (typeof isValid === "string") setError(isValid);
|
||||||
else if (!isValid) setError("Invalid input provided.");
|
else if (!isValid) setError("Invalid input provided.");
|
||||||
else {
|
else {
|
||||||
setError(null);
|
|
||||||
setState(newValue);
|
setState(newValue);
|
||||||
onChange(newValue);
|
onChange(newValue);
|
||||||
}
|
}
|
||||||
|
@ -30,12 +30,11 @@ import PluginModal from "@components/PluginSettings/PluginModal";
|
|||||||
import { Switch } from "@components/Switch";
|
import { Switch } from "@components/Switch";
|
||||||
import { ChangeList } from "@utils/ChangeList";
|
import { ChangeList } from "@utils/ChangeList";
|
||||||
import Logger from "@utils/Logger";
|
import Logger from "@utils/Logger";
|
||||||
import { Margins } from "@utils/margins";
|
|
||||||
import { classes, LazyComponent, useAwaiter } from "@utils/misc";
|
import { classes, LazyComponent, useAwaiter } from "@utils/misc";
|
||||||
import { openModalLazy } from "@utils/modal";
|
import { openModalLazy } from "@utils/modal";
|
||||||
import { Plugin } from "@utils/types";
|
import { Plugin } from "@utils/types";
|
||||||
import { findByCode, findByPropsLazy } from "@webpack";
|
import { findByCode, findByPropsLazy } from "@webpack";
|
||||||
import { Alerts, Button, Card, Forms, Parser, React, Select, Text, TextInput, Toasts, Tooltip } from "@webpack/common";
|
import { Alerts, Button, Card, Forms, Margins, Parser, React, Select, Text, TextInput, Toasts, Tooltip } from "@webpack/common";
|
||||||
|
|
||||||
import Plugins from "~plugins";
|
import Plugins from "~plugins";
|
||||||
|
|
||||||
@ -46,7 +45,6 @@ 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("inputDefault", "inputWrapper");
|
||||||
const ButtonClasses = findByPropsLazy("button", "disabled", "enabled");
|
|
||||||
|
|
||||||
const CogWheel = LazyComponent(() => findByCode("18.564C15.797 19.099 14.932 19.498 14 19.738V22H10V19.738C9.069"));
|
const CogWheel = LazyComponent(() => findByCode("18.564C15.797 19.099 14.932 19.498 14 19.738V22H10V19.738C9.069"));
|
||||||
const InfoIcon = LazyComponent(() => findByCode("4.4408921e-16 C4.4771525,-1.77635684e-15 4.4408921e-16"));
|
const InfoIcon = LazyComponent(() => findByCode("4.4408921e-16 C4.4771525,-1.77635684e-15 4.4408921e-16"));
|
||||||
@ -94,7 +92,7 @@ interface PluginCardProps extends React.HTMLProps<HTMLDivElement> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave, isNew }: PluginCardProps) {
|
function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave, isNew }: PluginCardProps) {
|
||||||
const settings = useSettings([`plugins.${plugin.name}.enabled`]).plugins[plugin.name];
|
const settings = useSettings([`plugins.${plugin.name}`]).plugins[plugin.name];
|
||||||
|
|
||||||
const isEnabled = () => settings.enabled ?? false;
|
const isEnabled = () => settings.enabled ?? false;
|
||||||
|
|
||||||
@ -155,7 +153,7 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe
|
|||||||
<Text variant="text-md/bold" className={cl("name")}>
|
<Text variant="text-md/bold" className={cl("name")}>
|
||||||
{plugin.name}{isNew && <Badge text="NEW" color="#ED4245" />}
|
{plugin.name}{isNew && <Badge text="NEW" color="#ED4245" />}
|
||||||
</Text>
|
</Text>
|
||||||
<button role="switch" onClick={() => openModal()} className={classes(ButtonClasses.button, cl("info-button"))}>
|
<button role="switch" onClick={() => openModal()} className={classes("button-12Fmur", cl("info-button"))}>
|
||||||
{plugin.options
|
{plugin.options
|
||||||
? <CogWheel />
|
? <CogWheel />
|
||||||
: <InfoIcon width="24" height="24" />}
|
: <InfoIcon width="24" height="24" />}
|
||||||
@ -298,15 +296,15 @@ export default ErrorBoundary.wrap(function PluginSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Forms.FormSection className={Margins.top16}>
|
<Forms.FormSection className={Margins.marginTop16}>
|
||||||
<ReloadRequiredCard required={changes.hasChanges} />
|
<ReloadRequiredCard required={changes.hasChanges} />
|
||||||
|
|
||||||
<Forms.FormTitle tag="h5" className={classes(Margins.top20, Margins.bottom8)}>
|
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
|
||||||
Filters
|
Filters
|
||||||
</Forms.FormTitle>
|
</Forms.FormTitle>
|
||||||
|
|
||||||
<div className={cl("filter-controls")}>
|
<div className={cl("filter-controls")}>
|
||||||
<TextInput autoFocus value={searchValue.value} placeholder="Search for a plugin..." onChange={onSearch} className={Margins.bottom20} />
|
<TextInput autoFocus value={searchValue.value} placeholder="Search for a plugin..." onChange={onSearch} className={Margins.marginBottom20} />
|
||||||
<div className={InputStyles.inputWrapper}>
|
<div className={InputStyles.inputWrapper}>
|
||||||
<Select
|
<Select
|
||||||
className={InputStyles.inputDefault}
|
className={InputStyles.inputDefault}
|
||||||
@ -323,15 +321,15 @@ export default ErrorBoundary.wrap(function PluginSettings() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Forms.FormTitle className={Margins.top20}>Plugins</Forms.FormTitle>
|
<Forms.FormTitle className={Margins.marginTop20}>Plugins</Forms.FormTitle>
|
||||||
|
|
||||||
<div className={cl("grid")}>
|
<div className={cl("grid")}>
|
||||||
{plugins}
|
{plugins}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Forms.FormDivider className={Margins.top20} />
|
<Forms.FormDivider className={Margins.marginTop20} />
|
||||||
|
|
||||||
<Forms.FormTitle tag="h5" className={classes(Margins.top20, Margins.bottom8)}>
|
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
|
||||||
Required Plugins
|
Required Plugins
|
||||||
</Forms.FormTitle>
|
</Forms.FormTitle>
|
||||||
<div className={cl("grid")}>
|
<div className={cl("grid")}>
|
||||||
|
@ -18,7 +18,6 @@
|
|||||||
|
|
||||||
import "./Switch.css";
|
import "./Switch.css";
|
||||||
|
|
||||||
import { classes } from "@utils/misc";
|
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
|
|
||||||
interface SwitchProps {
|
interface SwitchProps {
|
||||||
@ -34,7 +33,7 @@ const SwitchClasses = findByPropsLazy("slider", "input", "container");
|
|||||||
export function Switch({ checked, onChange, disabled }: SwitchProps) {
|
export function Switch({ checked, onChange, disabled }: SwitchProps) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={classes(SwitchClasses.container, "default-colors", checked ? SwitchClasses.checked : void 0)} style={{
|
<div className={`${SwitchClasses.container} default-colors`} style={{
|
||||||
backgroundColor: checked ? SWITCH_ON : SWITCH_OFF,
|
backgroundColor: checked ? SWITCH_ON : SWITCH_OFF,
|
||||||
opacity: disabled ? 0.3 : 1
|
opacity: disabled ? 0.3 : 1
|
||||||
}}>
|
}}>
|
||||||
|
@ -18,30 +18,28 @@
|
|||||||
|
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Flex } from "@components/Flex";
|
import { Flex } from "@components/Flex";
|
||||||
import { Margins } from "@utils/margins";
|
|
||||||
import { classes } from "@utils/misc";
|
import { classes } from "@utils/misc";
|
||||||
import { downloadSettingsBackup, uploadSettingsBackup } from "@utils/settingsSync";
|
import { downloadSettingsBackup, uploadSettingsBackup } from "@utils/settingsSync";
|
||||||
import { Button, Card, Forms, Text } from "@webpack/common";
|
import { Button, Card, Forms, Margins, Text } from "@webpack/common";
|
||||||
|
|
||||||
function BackupRestoreTab() {
|
function BackupRestoreTab() {
|
||||||
return (
|
return (
|
||||||
<Forms.FormSection title="Settings Sync" className={Margins.top16}>
|
<Forms.FormSection title="Settings Sync" className={Margins.marginTop16}>
|
||||||
<Card className={classes("vc-settings-card", "vc-backup-restore-card")}>
|
<Card className={classes("vc-settings-card", "vc-backup-restore-card")}>
|
||||||
<Flex flexDirection="column">
|
<Flex flexDirection="column">
|
||||||
<strong>Warning</strong>
|
<strong>Warning</strong>
|
||||||
<span>Importing a settings file will overwrite your current settings.</span>
|
<span>Importing a settings file will overwrite your current settings.</span>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Card>
|
</Card>
|
||||||
<Text variant="text-md/normal" className={Margins.bottom8}>
|
<Text variant="text-md/normal" className={Margins.marginBottom8}>
|
||||||
You can import and export your Vencord settings as a JSON file.
|
You can import and export your Vencord settings as a JSON file.
|
||||||
This allows you to easily transfer your settings to another device,
|
This allows you to easily transfer your settings to another device,
|
||||||
or recover your settings after reinstalling Vencord or Discord.
|
or recover your settings after reinstalling Vencord or Discord.
|
||||||
</Text>
|
</Text>
|
||||||
<Text variant="text-md/normal" className={Margins.bottom8}>
|
<Text variant="text-md/normal" className={Margins.marginBottom8}>
|
||||||
Settings Export contains:
|
Settings Export contains:
|
||||||
<ul>
|
<ul>
|
||||||
<li>— Custom QuickCSS</li>
|
<li>— Custom QuickCSS</li>
|
||||||
<li>— Theme Links</li>
|
|
||||||
<li>— Plugin Settings</li>
|
<li>— Plugin Settings</li>
|
||||||
</ul>
|
</ul>
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -1,164 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2023 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { showNotification } from "@api/Notifications";
|
|
||||||
import { Settings, useSettings } from "@api/settings";
|
|
||||||
import { CheckedTextInput } from "@components/CheckedTextInput";
|
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
|
||||||
import { Link } from "@components/Link";
|
|
||||||
import { authorizeCloud, cloudLogger, deauthorizeCloud, getCloudAuth, getCloudUrl } from "@utils/cloud";
|
|
||||||
import { Margins } from "@utils/margins";
|
|
||||||
import { deleteCloudSettings, getCloudSettings, putCloudSettings } from "@utils/settingsSync";
|
|
||||||
import { Alerts, Button, Forms, Switch, Tooltip } from "@webpack/common";
|
|
||||||
|
|
||||||
function validateUrl(url: string) {
|
|
||||||
try {
|
|
||||||
new URL(url);
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return "Invalid URL";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function eraseAllData() {
|
|
||||||
const res = await fetch(new URL("/v1/", getCloudUrl()), {
|
|
||||||
method: "DELETE",
|
|
||||||
headers: new Headers({
|
|
||||||
Authorization: await getCloudAuth()
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
cloudLogger.error(`Failed to erase data, API returned ${res.status}`);
|
|
||||||
showNotification({
|
|
||||||
title: "Cloud Integrations",
|
|
||||||
body: `Could not erase all data (API returned ${res.status}), please contact support.`,
|
|
||||||
color: "var(--red-360)"
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Settings.cloud.authenticated = false;
|
|
||||||
await deauthorizeCloud();
|
|
||||||
|
|
||||||
showNotification({
|
|
||||||
title: "Cloud Integrations",
|
|
||||||
body: "Successfully erased all data.",
|
|
||||||
color: "var(--green-360)"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function SettingsSyncSection() {
|
|
||||||
const { cloud } = useSettings(["cloud.authenticated", "cloud.settingsSync"]);
|
|
||||||
const sectionEnabled = cloud.authenticated && cloud.settingsSync;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Forms.FormSection title="Settings Sync" className={Margins.top16}>
|
|
||||||
<Forms.FormText variant="text-md/normal" className={Margins.bottom20}>
|
|
||||||
Synchronize your settings to the cloud. This allows easy synchronization across multiple devices with
|
|
||||||
minimal effort.
|
|
||||||
</Forms.FormText>
|
|
||||||
<Switch
|
|
||||||
key="cloud-sync"
|
|
||||||
disabled={!cloud.authenticated}
|
|
||||||
value={cloud.settingsSync}
|
|
||||||
onChange={v => { cloud.settingsSync = v; }}
|
|
||||||
>
|
|
||||||
Settings Sync
|
|
||||||
</Switch>
|
|
||||||
<div className="vc-cloud-settings-sync-grid">
|
|
||||||
<Button
|
|
||||||
size={Button.Sizes.SMALL}
|
|
||||||
disabled={!sectionEnabled}
|
|
||||||
onClick={() => putCloudSettings()}
|
|
||||||
>Sync to Cloud</Button>
|
|
||||||
<Tooltip text="This will overwrite your local settings with the ones on the cloud. Use wisely!">
|
|
||||||
{({ onMouseLeave, onMouseEnter }) => (
|
|
||||||
<Button
|
|
||||||
onMouseLeave={onMouseLeave}
|
|
||||||
onMouseEnter={onMouseEnter}
|
|
||||||
size={Button.Sizes.SMALL}
|
|
||||||
color={Button.Colors.RED}
|
|
||||||
disabled={!sectionEnabled}
|
|
||||||
onClick={() => getCloudSettings(true, true)}
|
|
||||||
>Sync from Cloud</Button>
|
|
||||||
)}
|
|
||||||
</Tooltip>
|
|
||||||
<Button
|
|
||||||
size={Button.Sizes.SMALL}
|
|
||||||
color={Button.Colors.RED}
|
|
||||||
disabled={!sectionEnabled}
|
|
||||||
onClick={() => deleteCloudSettings()}
|
|
||||||
>Delete Cloud Settings</Button>
|
|
||||||
</div>
|
|
||||||
</Forms.FormSection>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CloudTab() {
|
|
||||||
const settings = useSettings(["cloud.authenticated", "cloud.url"]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Forms.FormSection title="Cloud Settings" className={Margins.top16}>
|
|
||||||
<Forms.FormText variant="text-md/normal" className={Margins.bottom20}>
|
|
||||||
Vencord comes with a cloud integration that adds goodies like settings sync across devices.
|
|
||||||
It <Link href="https://vencord.dev/cloud/privacy">respects your privacy</Link>, and
|
|
||||||
the <Link href="https://github.com/Vencord/Backend">source code</Link> is AGPL 3.0 licensed so you
|
|
||||||
can host it yourself.
|
|
||||||
</Forms.FormText>
|
|
||||||
<Switch
|
|
||||||
key="backend"
|
|
||||||
value={settings.cloud.authenticated}
|
|
||||||
onChange={v => { v && authorizeCloud(); if (!v) settings.cloud.authenticated = v; }}
|
|
||||||
note="This will request authorization if you have not yet set up cloud integrations."
|
|
||||||
>
|
|
||||||
Enable Cloud Integrations
|
|
||||||
</Switch>
|
|
||||||
<Forms.FormTitle tag="h5">Backend URL</Forms.FormTitle>
|
|
||||||
<Forms.FormText className={Margins.bottom8}>
|
|
||||||
Which backend to use when using cloud integrations.
|
|
||||||
</Forms.FormText>
|
|
||||||
<CheckedTextInput
|
|
||||||
key="backendUrl"
|
|
||||||
value={settings.cloud.url}
|
|
||||||
onChange={v => { settings.cloud.url = v; settings.cloud.authenticated = false; deauthorizeCloud(); }}
|
|
||||||
validate={validateUrl}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
className={Margins.top8}
|
|
||||||
size={Button.Sizes.MEDIUM}
|
|
||||||
color={Button.Colors.RED}
|
|
||||||
disabled={!settings.cloud.authenticated}
|
|
||||||
onClick={() => Alerts.show({
|
|
||||||
title: "Are you sure?",
|
|
||||||
body: "Once your data is erased, we cannot recover it. There's no going back!",
|
|
||||||
onConfirm: eraseAllData,
|
|
||||||
confirmText: "Erase it!",
|
|
||||||
confirmColor: "vc-cloud-erase-data-danger-btn",
|
|
||||||
cancelText: "Nevermind"
|
|
||||||
})}
|
|
||||||
>Erase All Data</Button>
|
|
||||||
<Forms.FormDivider className={Margins.top16} />
|
|
||||||
</Forms.FormSection >
|
|
||||||
<SettingsSyncSection />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ErrorBoundary.wrap(CloudTab);
|
|
@ -19,10 +19,9 @@
|
|||||||
import { useSettings } from "@api/settings";
|
import { useSettings } from "@api/settings";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Link } from "@components/Link";
|
import { Link } from "@components/Link";
|
||||||
import { Margins } from "@utils/margins";
|
|
||||||
import { useAwaiter } from "@utils/misc";
|
import { useAwaiter } from "@utils/misc";
|
||||||
import { findLazy } from "@webpack";
|
import { findLazy } from "@webpack";
|
||||||
import { Card, Forms, React, TextArea } from "@webpack/common";
|
import { Card, Forms, Margins, React, TextArea } from "@webpack/common";
|
||||||
|
|
||||||
const TextAreaProps = findLazy(m => typeof m.textarea === "string");
|
const TextAreaProps = findLazy(m => typeof m.textarea === "string");
|
||||||
|
|
||||||
@ -52,7 +51,7 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Forms.FormTitle className={Margins.top20} tag="h5">Validator</Forms.FormTitle>
|
<Forms.FormTitle className={Margins.marginTop20} tag="h5">Validator</Forms.FormTitle>
|
||||||
<Forms.FormText>This section will tell you whether your themes can successfully be loaded</Forms.FormText>
|
<Forms.FormText>This section will tell you whether your themes can successfully be loaded</Forms.FormText>
|
||||||
<div>
|
<div>
|
||||||
{themeLinks.map(link => (
|
{themeLinks.map(link => (
|
||||||
@ -90,11 +89,11 @@ export default ErrorBoundary.wrap(function () {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card className="vc-settings-card vc-text-selectable">
|
<Card className="vc-settings-card">
|
||||||
<Forms.FormTitle tag="h5">Paste links to .theme.css files here</Forms.FormTitle>
|
<Forms.FormTitle tag="h5">Paste links to .css / .theme.css files here</Forms.FormTitle>
|
||||||
<Forms.FormText>One link per line</Forms.FormText>
|
<Forms.FormText>One link per line</Forms.FormText>
|
||||||
<Forms.FormText>Make sure to use the raw links or github.io links!</Forms.FormText>
|
<Forms.FormText>Make sure to use the raw links or github.io links!</Forms.FormText>
|
||||||
<Forms.FormDivider className={Margins.top8 + " " + Margins.bottom8} />
|
<Forms.FormDivider className={Margins.marginTop8 + " " + Margins.marginBottom8} />
|
||||||
<Forms.FormTitle tag="h5">Find Themes:</Forms.FormTitle>
|
<Forms.FormTitle tag="h5">Find Themes:</Forms.FormTitle>
|
||||||
<div style={{ marginBottom: ".5em" }}>
|
<div style={{ marginBottom: ".5em" }}>
|
||||||
<Link style={{ marginRight: ".5em" }} href="https://betterdiscord.app/themes">
|
<Link style={{ marginRight: ".5em" }} href="https://betterdiscord.app/themes">
|
||||||
@ -103,7 +102,7 @@ export default ErrorBoundary.wrap(function () {
|
|||||||
<Link href="https://github.com/search?q=discord+theme">GitHub</Link>
|
<Link href="https://github.com/search?q=discord+theme">GitHub</Link>
|
||||||
</div>
|
</div>
|
||||||
<Forms.FormText>If using the BD site, click on "Source" somewhere below the Download button</Forms.FormText>
|
<Forms.FormText>If using the BD site, click on "Source" somewhere below the Download button</Forms.FormText>
|
||||||
<Forms.FormText>In the GitHub repository of your theme, find X.theme.css, click on it, then click the "Raw" button</Forms.FormText>
|
<Forms.FormText>In the GitHub repository of your theme, find X.theme.css / X.css, click on it, then click the "Raw" button</Forms.FormText>
|
||||||
<Forms.FormText>
|
<Forms.FormText>
|
||||||
If the theme has configuration that requires you to edit the file:
|
If the theme has configuration that requires you to edit the file:
|
||||||
<ul>
|
<ul>
|
||||||
@ -116,9 +115,13 @@ export default ErrorBoundary.wrap(function () {
|
|||||||
</Card>
|
</Card>
|
||||||
<Forms.FormTitle tag="h5">Themes</Forms.FormTitle>
|
<Forms.FormTitle tag="h5">Themes</Forms.FormTitle>
|
||||||
<TextArea
|
<TextArea
|
||||||
|
style={{
|
||||||
|
padding: ".5em",
|
||||||
|
border: "1px solid var(--background-modifier-accent)"
|
||||||
|
}}
|
||||||
value={themeText}
|
value={themeText}
|
||||||
onChange={setThemeText}
|
onChange={e => setThemeText(e.currentTarget.value)}
|
||||||
className={`${TextAreaProps.textarea} vc-settings-theme-links`}
|
className={TextAreaProps.textarea}
|
||||||
placeholder="Theme Links"
|
placeholder="Theme Links"
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
|
@ -22,11 +22,9 @@ import { ErrorCard } from "@components/ErrorCard";
|
|||||||
import { Flex } from "@components/Flex";
|
import { Flex } from "@components/Flex";
|
||||||
import { handleComponentFailed } from "@components/handleComponentFailed";
|
import { handleComponentFailed } from "@components/handleComponentFailed";
|
||||||
import { Link } from "@components/Link";
|
import { Link } from "@components/Link";
|
||||||
import { Margins } from "@utils/margins";
|
|
||||||
import { classes, useAwaiter } from "@utils/misc";
|
import { classes, useAwaiter } from "@utils/misc";
|
||||||
import { relaunch } from "@utils/native";
|
|
||||||
import { changes, checkForUpdates, getRepo, isNewer, rebuild, update, updateError, UpdateLogger } from "@utils/updater";
|
import { changes, checkForUpdates, getRepo, isNewer, rebuild, update, updateError, UpdateLogger } from "@utils/updater";
|
||||||
import { Alerts, Button, Card, Forms, Parser, React, Switch, Toasts } from "@webpack/common";
|
import { Alerts, Button, Card, Forms, Margins, Parser, React, Switch, Toasts } from "@webpack/common";
|
||||||
|
|
||||||
import gitHash from "~git-hash";
|
import gitHash from "~git-hash";
|
||||||
|
|
||||||
@ -111,21 +109,21 @@ function Updatable(props: CommonProps) {
|
|||||||
</ErrorCard>
|
</ErrorCard>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Forms.FormText className={Margins.bottom8}>
|
<Forms.FormText className={Margins.marginBottom8}>
|
||||||
{isOutdated ? `There are ${updates.length} Updates` : "Up to Date!"}
|
{isOutdated ? `There are ${updates.length} Updates` : "Up to Date!"}
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isOutdated && <Changes updates={updates} {...props} />}
|
{isOutdated && <Changes updates={updates} {...props} />}
|
||||||
|
|
||||||
<Flex className={classes(Margins.bottom8, Margins.top8)}>
|
<Flex className={classes(Margins.marginBottom8, Margins.marginTop8)}>
|
||||||
{isOutdated && <Button
|
{isOutdated && <Button
|
||||||
size={Button.Sizes.SMALL}
|
size={Button.Sizes.SMALL}
|
||||||
disabled={isUpdating || isChecking}
|
disabled={isUpdating || isChecking}
|
||||||
onClick={withDispatcher(setIsUpdating, async () => {
|
onClick={withDispatcher(setIsUpdating, async () => {
|
||||||
if (await update()) {
|
if (await update()) {
|
||||||
setUpdates([]);
|
setUpdates([]);
|
||||||
await rebuild();
|
const needFullRestart = await rebuild();
|
||||||
await new Promise<void>(r => {
|
await new Promise<void>(r => {
|
||||||
Alerts.show({
|
Alerts.show({
|
||||||
title: "Update Success!",
|
title: "Update Success!",
|
||||||
@ -133,7 +131,10 @@ function Updatable(props: CommonProps) {
|
|||||||
confirmText: "Restart",
|
confirmText: "Restart",
|
||||||
cancelText: "Not now!",
|
cancelText: "Not now!",
|
||||||
onConfirm() {
|
onConfirm() {
|
||||||
relaunch();
|
if (needFullRestart)
|
||||||
|
window.DiscordNative.app.relaunch();
|
||||||
|
else
|
||||||
|
location.reload();
|
||||||
r();
|
r();
|
||||||
},
|
},
|
||||||
onCancel: r
|
onCancel: r
|
||||||
@ -174,7 +175,7 @@ function Updatable(props: CommonProps) {
|
|||||||
function Newer(props: CommonProps) {
|
function Newer(props: CommonProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Forms.FormText className={Margins.bottom8}>
|
<Forms.FormText className={Margins.marginBottom8}>
|
||||||
Your local copy has more recent commits. Please stash or reset them.
|
Your local copy has more recent commits. Please stash or reset them.
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
<Changes {...props} updates={changes} />
|
<Changes {...props} updates={changes} />
|
||||||
@ -183,7 +184,7 @@ function Newer(props: CommonProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Updater() {
|
function Updater() {
|
||||||
const settings = useSettings(["notifyAboutUpdates", "autoUpdate", "autoUpdateNotification"]);
|
const settings = useSettings(["notifyAboutUpdates", "autoUpdate"]);
|
||||||
|
|
||||||
const [repo, err, repoPending] = useAwaiter(getRepo, { fallbackValue: "Loading..." });
|
const [repo, err, repoPending] = useAwaiter(getRepo, { fallbackValue: "Loading..." });
|
||||||
|
|
||||||
@ -198,12 +199,12 @@ function Updater() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Forms.FormSection className={Margins.top16}>
|
<Forms.FormSection className={Margins.marginTop16}>
|
||||||
<Forms.FormTitle tag="h5">Updater Settings</Forms.FormTitle>
|
<Forms.FormTitle tag="h5">Updater Settings</Forms.FormTitle>
|
||||||
<Switch
|
<Switch
|
||||||
value={settings.notifyAboutUpdates}
|
value={settings.notifyAboutUpdates}
|
||||||
onChange={(v: boolean) => settings.notifyAboutUpdates = v}
|
onChange={(v: boolean) => settings.notifyAboutUpdates = v}
|
||||||
note="Shows a notification on startup"
|
note="Shows a toast on startup"
|
||||||
disabled={settings.autoUpdate}
|
disabled={settings.autoUpdate}
|
||||||
>
|
>
|
||||||
Get notified about new updates
|
Get notified about new updates
|
||||||
@ -215,32 +216,16 @@ function Updater() {
|
|||||||
>
|
>
|
||||||
Automatically update
|
Automatically update
|
||||||
</Switch>
|
</Switch>
|
||||||
<Switch
|
|
||||||
value={settings.autoUpdateNotification}
|
|
||||||
onChange={(v: boolean) => settings.autoUpdateNotification = v}
|
|
||||||
note="Shows a notification when Vencord automatically updates"
|
|
||||||
disabled={!settings.autoUpdate}
|
|
||||||
>
|
|
||||||
Get notified when an automatic update completes
|
|
||||||
</Switch>
|
|
||||||
|
|
||||||
<Forms.FormTitle tag="h5">Repo</Forms.FormTitle>
|
<Forms.FormTitle tag="h5">Repo</Forms.FormTitle>
|
||||||
|
|
||||||
<Forms.FormText className="vc-text-selectable">
|
<Forms.FormText>{repoPending ? repo : err ? "Failed to retrieve - check console" : (
|
||||||
{repoPending
|
|
||||||
? repo
|
|
||||||
: err
|
|
||||||
? "Failed to retrieve - check console"
|
|
||||||
: (
|
|
||||||
<Link href={repo}>
|
<Link href={repo}>
|
||||||
{repo.split("/").slice(-2).join("/")}
|
{repo.split("/").slice(-2).join("/")}
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)} (<HashLink hash={gitHash} repo={repo} disabled={repoPending} />)</Forms.FormText>
|
||||||
}
|
|
||||||
{" "}(<HashLink hash={gitHash} repo={repo} disabled={repoPending} />)
|
|
||||||
</Forms.FormText>
|
|
||||||
|
|
||||||
<Forms.FormDivider className={Margins.top8 + " " + Margins.bottom8} />
|
<Forms.FormDivider className={Margins.marginTop8 + " " + Margins.marginBottom8} />
|
||||||
|
|
||||||
<Forms.FormTitle tag="h5">Updates</Forms.FormTitle>
|
<Forms.FormTitle tag="h5">Updates</Forms.FormTitle>
|
||||||
|
|
||||||
|
@ -17,8 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import { openNotificationLogModal } from "@api/Notifications/notificationLog";
|
import { useSettings } from "@api/settings";
|
||||||
import { Settings, 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 ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
@ -26,7 +25,6 @@ import { ErrorCard } from "@components/ErrorCard";
|
|||||||
import IpcEvents from "@utils/IpcEvents";
|
import IpcEvents from "@utils/IpcEvents";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { identity, useAwaiter } from "@utils/misc";
|
import { identity, useAwaiter } from "@utils/misc";
|
||||||
import { relaunch, showItemInFolder } from "@utils/native";
|
|
||||||
import { Button, Card, Forms, React, Select, Slider, Switch } from "@webpack/common";
|
import { Button, Card, Forms, React, Select, Slider, Switch } from "@webpack/common";
|
||||||
|
|
||||||
const cl = classNameFactory("vc-settings-");
|
const cl = classNameFactory("vc-settings-");
|
||||||
@ -43,11 +41,11 @@ function VencordSettings() {
|
|||||||
fallbackValue: "Loading..."
|
fallbackValue: "Loading..."
|
||||||
});
|
});
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
|
const notifSettings = settings.notifications;
|
||||||
|
|
||||||
const donateImage = React.useMemo(() => Math.random() > 0.5 ? DEFAULT_DONATE_IMAGE : SHIGGY_DONATE_IMAGE, []);
|
const donateImage = React.useMemo(() => Math.random() > 0.5 ? DEFAULT_DONATE_IMAGE : SHIGGY_DONATE_IMAGE, []);
|
||||||
|
|
||||||
const isWindows = navigator.platform.toLowerCase().startsWith("win");
|
const isWindows = navigator.platform.toLowerCase().startsWith("win");
|
||||||
const isMac = navigator.platform.toLowerCase().startsWith("mac");
|
|
||||||
|
|
||||||
const Switches: Array<false | {
|
const Switches: Array<false | {
|
||||||
key: KeysOfType<typeof settings, boolean>;
|
key: KeysOfType<typeof settings, boolean>;
|
||||||
@ -65,16 +63,12 @@ function VencordSettings() {
|
|||||||
title: "Enable React Developer Tools",
|
title: "Enable React Developer Tools",
|
||||||
note: "Requires a full restart"
|
note: "Requires a full restart"
|
||||||
},
|
},
|
||||||
!IS_WEB && (!IS_DISCORD_DESKTOP || !isWindows ? {
|
!IS_WEB && !isWindows && {
|
||||||
key: "frameless",
|
key: "frameless",
|
||||||
title: "Disable the window frame",
|
title: "Disable the window frame",
|
||||||
note: "Requires a full restart"
|
note: "Requires a full restart"
|
||||||
} : {
|
},
|
||||||
key: "winNativeTitleBar",
|
!IS_WEB && {
|
||||||
title: "Use Windows' native title bar instead of Discord's custom one",
|
|
||||||
note: "Requires a full restart"
|
|
||||||
}),
|
|
||||||
!IS_WEB && false /* This causes electron to freeze / white screen for some people */ && {
|
|
||||||
key: "transparent",
|
key: "transparent",
|
||||||
title: "Enable window transparency",
|
title: "Enable window transparency",
|
||||||
note: "Requires a full restart"
|
note: "Requires a full restart"
|
||||||
@ -83,16 +77,6 @@ function VencordSettings() {
|
|||||||
key: "winCtrlQ",
|
key: "winCtrlQ",
|
||||||
title: "Register Ctrl+Q as shortcut to close Discord (Alternative to Alt+F4)",
|
title: "Register Ctrl+Q as shortcut to close Discord (Alternative to Alt+F4)",
|
||||||
note: "Requires a full restart"
|
note: "Requires a full restart"
|
||||||
},
|
|
||||||
IS_DISCORD_DESKTOP && {
|
|
||||||
key: "disableMinSize",
|
|
||||||
title: "Disable minimum window size",
|
|
||||||
note: "Requires a full restart"
|
|
||||||
},
|
|
||||||
IS_DISCORD_DESKTOP && isMac && {
|
|
||||||
key: "macosTranslucency",
|
|
||||||
title: "Enable translucent window",
|
|
||||||
note: "Requires a full restart"
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -111,7 +95,7 @@ function VencordSettings() {
|
|||||||
) : (
|
) : (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Button
|
<Button
|
||||||
onClick={relaunch}
|
onClick={() => window.DiscordNative.app.relaunch()}
|
||||||
size={Button.Sizes.SMALL}>
|
size={Button.Sizes.SMALL}>
|
||||||
Restart Client
|
Restart Client
|
||||||
</Button>
|
</Button>
|
||||||
@ -122,7 +106,7 @@ function VencordSettings() {
|
|||||||
Open QuickCSS File
|
Open QuickCSS File
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => showItemInFolder(settingsDir)}
|
onClick={() => window.DiscordNative.fileManager.showItemInFolder(settingsDir)}
|
||||||
size={Button.Sizes.SMALL}
|
size={Button.Sizes.SMALL}
|
||||||
disabled={settingsDirPending}>
|
disabled={settingsDirPending}>
|
||||||
Open Settings Folder
|
Open Settings Folder
|
||||||
@ -157,16 +141,8 @@ function VencordSettings() {
|
|||||||
</Forms.FormSection>
|
</Forms.FormSection>
|
||||||
|
|
||||||
|
|
||||||
{typeof Notification !== "undefined" && <NotificationSection settings={settings.notifications} />}
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function NotificationSection({ settings }: { settings: typeof Settings["notifications"]; }) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Forms.FormTitle tag="h5">Notification Style</Forms.FormTitle>
|
<Forms.FormTitle tag="h5">Notification Style</Forms.FormTitle>
|
||||||
{settings.useNative !== "never" && Notification?.permission === "denied" && (
|
{notifSettings.useNative !== "never" && Notification.permission === "denied" && (
|
||||||
<ErrorCard style={{ padding: "1em" }} className={Margins.bottom8}>
|
<ErrorCard style={{ padding: "1em" }} className={Margins.bottom8}>
|
||||||
<Forms.FormTitle tag="h5">Desktop Notification Permission denied</Forms.FormTitle>
|
<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>
|
<Forms.FormText>You have denied Notification Permissions. Thus, Desktop notifications will not work!</Forms.FormText>
|
||||||
@ -185,66 +161,44 @@ function NotificationSection({ settings }: { settings: typeof Settings["notifica
|
|||||||
{ label: "Only use Desktop notifications when Discord is not focused", value: "not-focused", default: true },
|
{ label: "Only use Desktop notifications when Discord is not focused", value: "not-focused", default: true },
|
||||||
{ label: "Always use Desktop notifications", value: "always" },
|
{ label: "Always use Desktop notifications", value: "always" },
|
||||||
{ label: "Always use Vencord notifications", value: "never" },
|
{ label: "Always use Vencord notifications", value: "never" },
|
||||||
] satisfies Array<{ value: typeof settings["useNative"]; } & Record<string, any>>}
|
]satisfies Array<{ value: typeof settings["notifications"]["useNative"]; } & Record<string, any>>}
|
||||||
closeOnSelect={true}
|
closeOnSelect={true}
|
||||||
select={v => settings.useNative = v}
|
select={v => notifSettings.useNative = v}
|
||||||
isSelected={v => v === settings.useNative}
|
isSelected={v => v === notifSettings.useNative}
|
||||||
serialize={identity}
|
serialize={identity}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Position</Forms.FormTitle>
|
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Position</Forms.FormTitle>
|
||||||
<Select
|
<Select
|
||||||
isDisabled={settings.useNative === "always"}
|
isDisabled={notifSettings.useNative === "always"}
|
||||||
placeholder="Notification Position"
|
placeholder="Notification Position"
|
||||||
options={[
|
options={[
|
||||||
{ label: "Bottom Right", value: "bottom-right", default: true },
|
{ label: "Bottom Right", value: "bottom-right", default: true },
|
||||||
{ label: "Top Right", value: "top-right" },
|
{ label: "Top Right", value: "top-right" },
|
||||||
] satisfies Array<{ value: typeof settings["position"]; } & Record<string, any>>}
|
]satisfies Array<{ value: typeof settings["notifications"]["position"]; } & Record<string, any>>}
|
||||||
select={v => settings.position = v}
|
select={v => notifSettings.position = v}
|
||||||
isSelected={v => v === settings.position}
|
isSelected={v => v === notifSettings.position}
|
||||||
serialize={identity}
|
serialize={identity}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Timeout</Forms.FormTitle>
|
<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>
|
<Forms.FormText className={Margins.bottom16}>Set to 0s to never automatically time out</Forms.FormText>
|
||||||
<Slider
|
<Slider
|
||||||
disabled={settings.useNative === "always"}
|
disabled={notifSettings.useNative === "always"}
|
||||||
markers={[0, 1000, 2500, 5000, 10_000, 20_000]}
|
markers={[0, 1000, 2500, 5000, 10_000, 20_000]}
|
||||||
minValue={0}
|
minValue={0}
|
||||||
maxValue={20_000}
|
maxValue={20_000}
|
||||||
initialValue={settings.timeout}
|
initialValue={notifSettings.timeout}
|
||||||
onValueChange={v => settings.timeout = v}
|
onValueChange={v => notifSettings.timeout = v}
|
||||||
onValueRender={v => (v / 1000).toFixed(2) + "s"}
|
onValueRender={v => (v / 1000).toFixed(2) + "s"}
|
||||||
onMarkerRender={v => (v / 1000) + "s"}
|
onMarkerRender={v => (v / 1000) + "s"}
|
||||||
stickToMarkers={false}
|
stickToMarkers={false}
|
||||||
/>
|
/>
|
||||||
|
</React.Fragment>
|
||||||
<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>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
interface DonateCardProps {
|
interface DonateCardProps {
|
||||||
image: string;
|
image: string;
|
||||||
}
|
}
|
||||||
|
@ -20,12 +20,10 @@ import "./settingsStyles.css";
|
|||||||
|
|
||||||
import { classNameFactory } from "@api/Styles";
|
import { classNameFactory } from "@api/Styles";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { handleComponentFailed } from "@components/handleComponentFailed";
|
import { findByCodeLazy } from "@webpack";
|
||||||
import { isMobile } from "@utils/misc";
|
import { Forms, SettingsRouter, Text } from "@webpack/common";
|
||||||
import { Forms, SettingsRouter, TabBar, Text } from "@webpack/common";
|
|
||||||
|
|
||||||
import BackupRestoreTab from "./BackupRestoreTab";
|
import BackupRestoreTab from "./BackupRestoreTab";
|
||||||
import CloudTab from "./CloudTab";
|
|
||||||
import PluginsTab from "./PluginsTab";
|
import PluginsTab from "./PluginsTab";
|
||||||
import ThemesTab from "./ThemesTab";
|
import ThemesTab from "./ThemesTab";
|
||||||
import Updater from "./Updater";
|
import Updater from "./Updater";
|
||||||
@ -33,6 +31,8 @@ import VencordSettings from "./VencordTab";
|
|||||||
|
|
||||||
const cl = classNameFactory("vc-settings-");
|
const cl = classNameFactory("vc-settings-");
|
||||||
|
|
||||||
|
const TabBar = findByCodeLazy('[role="tab"][aria-disabled="false"]');
|
||||||
|
|
||||||
interface SettingsProps {
|
interface SettingsProps {
|
||||||
tab: string;
|
tab: string;
|
||||||
}
|
}
|
||||||
@ -47,8 +47,7 @@ const SettingsTabs: Record<string, SettingsTab> = {
|
|||||||
VencordPlugins: { name: "Plugins", component: () => <PluginsTab /> },
|
VencordPlugins: { name: "Plugins", component: () => <PluginsTab /> },
|
||||||
VencordThemes: { name: "Themes", component: () => <ThemesTab /> },
|
VencordThemes: { name: "Themes", component: () => <ThemesTab /> },
|
||||||
VencordUpdater: { name: "Updater" }, // Only show updater if IS_WEB is false
|
VencordUpdater: { name: "Updater" }, // Only show updater if IS_WEB is false
|
||||||
VencordCloud: { name: "Cloud", component: () => <CloudTab /> },
|
VencordSettingsSync: { name: "Backup & Restore", component: () => <BackupRestoreTab /> },
|
||||||
VencordSettingsSync: { name: "Backup & Restore", component: () => <BackupRestoreTab /> }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!IS_WEB) SettingsTabs.VencordUpdater.component = () => Updater && <Updater />;
|
if (!IS_WEB) SettingsTabs.VencordUpdater.component = () => Updater && <Updater />;
|
||||||
@ -56,17 +55,14 @@ if (!IS_WEB) SettingsTabs.VencordUpdater.component = () => Updater && <Updater /
|
|||||||
function Settings(props: SettingsProps) {
|
function Settings(props: SettingsProps) {
|
||||||
const { tab = "VencordSettings" } = props;
|
const { tab = "VencordSettings" } = props;
|
||||||
|
|
||||||
const CurrentTab = SettingsTabs[tab]?.component ?? null;
|
const CurrentTab = SettingsTabs[tab]?.component;
|
||||||
if (isMobile) {
|
|
||||||
return CurrentTab && <CurrentTab />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Forms.FormSection>
|
return <Forms.FormSection>
|
||||||
<Text variant="heading-lg/semibold" style={{ color: "var(--header-primary)" }} tag="h2">Vencord Settings</Text>
|
<Text variant="heading-md/normal" tag="h2">Vencord Settings</Text>
|
||||||
|
|
||||||
<TabBar
|
<TabBar
|
||||||
type="top"
|
type={TabBar.Types.TOP}
|
||||||
look="brand"
|
look={TabBar.Looks.BRAND}
|
||||||
className={cl("tab-bar")}
|
className={cl("tab-bar")}
|
||||||
selectedItem={tab}
|
selectedItem={tab}
|
||||||
onItemSelect={SettingsRouter.open}
|
onItemSelect={SettingsRouter.open}
|
||||||
@ -87,7 +83,7 @@ function Settings(props: SettingsProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function (props: SettingsProps) {
|
export default function (props: SettingsProps) {
|
||||||
return <ErrorBoundary onError={handleComponentFailed}>
|
return <ErrorBoundary>
|
||||||
<Settings tab={props.tab} />
|
<Settings tab={props.tab} />
|
||||||
</ErrorBoundary>;
|
</ErrorBoundary>;
|
||||||
}
|
}
|
||||||
|
@ -38,31 +38,3 @@
|
|||||||
color: var(--info-warning-text);
|
color: var(--info-warning-text);
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-settings-theme-links {
|
|
||||||
/* Needed to fix bad themes that hide certain textarea elements for whatever eldritch reason */
|
|
||||||
display: inline-block !important;
|
|
||||||
color: var(--text-normal) !important;
|
|
||||||
padding: 0.5em;
|
|
||||||
border: 1px solid var(--background-modifier-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-cloud-settings-sync-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
grid-gap: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-cloud-erase-data-danger-btn {
|
|
||||||
color: var(--white-500);
|
|
||||||
background-color: var(--button-danger-background);
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-text-selectable,
|
|
||||||
.vc-text-selectable :not(a, button) {
|
|
||||||
/* make text selectable, silly discord makes the entirety of settings not selectable */
|
|
||||||
user-select: text;
|
|
||||||
|
|
||||||
/* discord also sets cursor: default which prevents the cursor from showing as text */
|
|
||||||
cursor: initial;
|
|
||||||
}
|
|
||||||
|
@ -16,12 +16,29 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { maybePromptToUpdate } from "@utils/updater";
|
import { isOutdated, rebuild, update } from "@utils/updater";
|
||||||
|
|
||||||
export function handleComponentFailed() {
|
export async function handleComponentFailed() {
|
||||||
maybePromptToUpdate(
|
if (isOutdated) {
|
||||||
|
setImmediate(async () => {
|
||||||
|
const wantsUpdate = confirm(
|
||||||
"Uh Oh! Failed to render this Page." +
|
"Uh Oh! Failed to render this Page." +
|
||||||
" However, there is an update available that might fix it." +
|
" However, there is an update available that might fix it." +
|
||||||
" Would you like to update and restart now?"
|
" Would you like to update and restart now?"
|
||||||
);
|
);
|
||||||
|
if (wantsUpdate) {
|
||||||
|
try {
|
||||||
|
await update();
|
||||||
|
await rebuild();
|
||||||
|
if (IS_WEB)
|
||||||
|
location.reload();
|
||||||
|
else
|
||||||
|
DiscordNative.app.relaunch();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
alert("That also failed :( Try updating or reinstalling with the installer!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Vencord QuickCSS Editor</title>
|
<title>QuickCss Editor</title>
|
||||||
<link rel="stylesheet" data-name="vs/editor/editor.main"
|
<link rel="stylesheet" data-name="vs/editor/editor.main"
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs/editor/editor.main.min.css">
|
href="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs/editor/editor.main.min.css">
|
||||||
<style>
|
<style>
|
||||||
|
7
src/globals.d.ts
vendored
7
src/globals.d.ts
vendored
@ -35,8 +35,6 @@ declare global {
|
|||||||
export var IS_WEB: boolean;
|
export var IS_WEB: boolean;
|
||||||
export var IS_DEV: boolean;
|
export var IS_DEV: boolean;
|
||||||
export var IS_STANDALONE: boolean;
|
export var IS_STANDALONE: boolean;
|
||||||
export var IS_DISCORD_DESKTOP: boolean;
|
|
||||||
export var IS_VENCORD_DESKTOP: boolean;
|
|
||||||
|
|
||||||
export var VencordNative: typeof import("./VencordNative").default;
|
export var VencordNative: typeof import("./VencordNative").default;
|
||||||
export var Vencord: typeof import("./Vencord");
|
export var Vencord: typeof import("./Vencord");
|
||||||
@ -53,11 +51,10 @@ declare global {
|
|||||||
* Only available when running in Electron, undefined on web.
|
* Only available when running in Electron, undefined on web.
|
||||||
* Thus, avoid using this or only use it inside an {@link IS_WEB} guard.
|
* Thus, avoid using this or only use it inside an {@link IS_WEB} guard.
|
||||||
*
|
*
|
||||||
* If you really must use it, mark your plugin as Desktop App only by naming it Foo.desktop.ts(x)
|
* If you really must use it, mark your plugin as Desktop App only via
|
||||||
|
* `target: "DESKTOP"`
|
||||||
*/
|
*/
|
||||||
export var DiscordNative: any;
|
export var DiscordNative: any;
|
||||||
export var VencordDesktop: any;
|
|
||||||
export var VencordDesktopNative: any;
|
|
||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
webpackChunkdiscord_app: {
|
webpackChunkdiscord_app: {
|
||||||
|
@ -33,5 +33,3 @@ export const ALLOWED_PROTOCOLS = [
|
|||||||
"steam:",
|
"steam:",
|
||||||
"spotify:"
|
"spotify:"
|
||||||
];
|
];
|
||||||
|
|
||||||
export const IS_VANILLA = /* @__PURE__ */ process.argv.includes("--vanilla");
|
|
@ -28,7 +28,7 @@ import { join } from "path";
|
|||||||
|
|
||||||
import monacoHtml from "~fileContent/../components/monacoWin.html;base64";
|
import monacoHtml from "~fileContent/../components/monacoWin.html;base64";
|
||||||
|
|
||||||
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, SETTINGS_FILE } from "./utils/constants";
|
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, SETTINGS_FILE } from "./constants";
|
||||||
|
|
||||||
mkdirSync(SETTINGS_DIR, { recursive: true });
|
mkdirSync(SETTINGS_DIR, { recursive: true });
|
||||||
|
|
||||||
@ -44,14 +44,6 @@ export function readSettings() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSettings(): typeof import("@api/settings").Settings {
|
|
||||||
try {
|
|
||||||
return JSON.parse(readSettings());
|
|
||||||
} catch {
|
|
||||||
return {} as any;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ipcMain.handle(IpcEvents.OPEN_QUICKCSS, () => shell.openPath(QUICKCSS_PATH));
|
ipcMain.handle(IpcEvents.OPEN_QUICKCSS, () => shell.openPath(QUICKCSS_PATH));
|
||||||
|
|
||||||
ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => {
|
ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => {
|
||||||
@ -93,7 +85,7 @@ export function initIpc(mainWindow: BrowserWindow) {
|
|||||||
|
|
||||||
ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => {
|
ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => {
|
||||||
const win = new BrowserWindow({
|
const win = new BrowserWindow({
|
||||||
title: "Vencord QuickCSS Editor",
|
title: "QuickCss Editor",
|
||||||
autoHideMenuBar: true,
|
autoHideMenuBar: true,
|
||||||
darkTheme: true,
|
darkTheme: true,
|
||||||
webPreferences: {
|
webPreferences: {
|
@ -16,13 +16,28 @@
|
|||||||
* 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 { createHash } from "crypto";
|
||||||
|
import { createReadStream } from "fs";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
export const VENCORD_FILES = [
|
export async function calculateHashes() {
|
||||||
IS_DISCORD_DESKTOP ? "patcher.js" : "vencordDesktopMain.js",
|
const hashes = {} as Record<string, string>;
|
||||||
"preload.js",
|
|
||||||
IS_DISCORD_DESKTOP ? "renderer.js" : "vencordDesktopRenderer.js",
|
await Promise.all(
|
||||||
"renderer.css"
|
["patcher.js", "preload.js", "renderer.js", "renderer.css"].map(file => new Promise<void>(r => {
|
||||||
];
|
const fis = createReadStream(join(__dirname, file));
|
||||||
|
const hash = createHash("sha1", { encoding: "hex" });
|
||||||
|
fis.once("end", () => {
|
||||||
|
hash.end();
|
||||||
|
hashes[file] = hash.read();
|
||||||
|
r();
|
||||||
|
});
|
||||||
|
fis.pipe(hash);
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
return hashes;
|
||||||
|
}
|
||||||
|
|
||||||
export function serializeErrors(func: (...args: any[]) => any) {
|
export function serializeErrors(func: (...args: any[]) => any) {
|
||||||
return async function () {
|
return async function () {
|
@ -22,7 +22,7 @@ import { ipcMain } from "electron";
|
|||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import { promisify } from "util";
|
import { promisify } from "util";
|
||||||
|
|
||||||
import { serializeErrors } from "./common";
|
import { calculateHashes, serializeErrors } from "./common";
|
||||||
|
|
||||||
const VENCORD_SRC_DIR = join(__dirname, "..");
|
const VENCORD_SRC_DIR = join(__dirname, "..");
|
||||||
|
|
||||||
@ -76,6 +76,7 @@ async function build() {
|
|||||||
return !res.stderr.includes("Build failed");
|
return !res.stderr.includes("Build failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ipcMain.handle(IpcEvents.GET_HASHES, serializeErrors(calculateHashes));
|
||||||
ipcMain.handle(IpcEvents.GET_REPO, serializeErrors(getRepo));
|
ipcMain.handle(IpcEvents.GET_REPO, serializeErrors(getRepo));
|
||||||
ipcMain.handle(IpcEvents.GET_UPDATES, serializeErrors(calculateGitChanges));
|
ipcMain.handle(IpcEvents.GET_UPDATES, serializeErrors(calculateGitChanges));
|
||||||
ipcMain.handle(IpcEvents.UPDATE, serializeErrors(pull));
|
ipcMain.handle(IpcEvents.UPDATE, serializeErrors(pull));
|
@ -25,8 +25,8 @@ import { join } from "path";
|
|||||||
import gitHash from "~git-hash";
|
import gitHash from "~git-hash";
|
||||||
import gitRemote from "~git-remote";
|
import gitRemote from "~git-remote";
|
||||||
|
|
||||||
import { get } from "../utils/simpleGet";
|
import { get } from "../simpleGet";
|
||||||
import { serializeErrors, VENCORD_FILES } from "./common";
|
import { calculateHashes, serializeErrors } from "./common";
|
||||||
|
|
||||||
const API_BASE = `https://api.github.com/repos/${gitRemote}`;
|
const API_BASE = `https://api.github.com/repos/${gitRemote}`;
|
||||||
let PendingUpdates = [] as [string, string][];
|
let PendingUpdates = [] as [string, string][];
|
||||||
@ -66,7 +66,7 @@ async function fetchUpdates() {
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
data.assets.forEach(({ name, browser_download_url }) => {
|
data.assets.forEach(({ name, browser_download_url }) => {
|
||||||
if (VENCORD_FILES.some(s => name.startsWith(s))) {
|
if (["patcher.js", "preload.js", "renderer.js", "renderer.css"].some(s => name.startsWith(s))) {
|
||||||
PendingUpdates.push([name, browser_download_url]);
|
PendingUpdates.push([name, browser_download_url]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -75,15 +75,13 @@ async function fetchUpdates() {
|
|||||||
|
|
||||||
async function applyUpdates() {
|
async function applyUpdates() {
|
||||||
await Promise.all(PendingUpdates.map(
|
await Promise.all(PendingUpdates.map(
|
||||||
async ([name, data]) => writeFile(
|
async ([name, data]) => writeFile(join(__dirname, name), await get(data)))
|
||||||
join(__dirname, name),
|
);
|
||||||
await get(data)
|
|
||||||
)
|
|
||||||
));
|
|
||||||
PendingUpdates = [];
|
PendingUpdates = [];
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ipcMain.handle(IpcEvents.GET_HASHES, serializeErrors(calculateHashes));
|
||||||
ipcMain.handle(IpcEvents.GET_REPO, serializeErrors(() => `https://github.com/${gitRemote}`));
|
ipcMain.handle(IpcEvents.GET_REPO, serializeErrors(() => `https://github.com/${gitRemote}`));
|
||||||
ipcMain.handle(IpcEvents.GET_UPDATES, serializeErrors(calculateGitChanges));
|
ipcMain.handle(IpcEvents.GET_UPDATES, serializeErrors(calculateGitChanges));
|
||||||
ipcMain.handle(IpcEvents.UPDATE, serializeErrors(fetchUpdates));
|
ipcMain.handle(IpcEvents.UPDATE, serializeErrors(fetchUpdates));
|
@ -1,110 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2023 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { app, protocol, session } from "electron";
|
|
||||||
import { join } from "path";
|
|
||||||
|
|
||||||
import { getSettings } from "./ipcMain";
|
|
||||||
import { IS_VANILLA } from "./utils/constants";
|
|
||||||
import { installExt } from "./utils/extensions";
|
|
||||||
|
|
||||||
if (IS_VENCORD_DESKTOP || !IS_VANILLA) {
|
|
||||||
app.whenReady().then(() => {
|
|
||||||
// Source Maps! Maybe there's a better way but since the renderer is executed
|
|
||||||
// from a string I don't think any other form of sourcemaps would work
|
|
||||||
protocol.registerFileProtocol("vencord", ({ url: unsafeUrl }, cb) => {
|
|
||||||
let url = unsafeUrl.slice("vencord://".length);
|
|
||||||
if (url.endsWith("/")) url = url.slice(0, -1);
|
|
||||||
switch (url) {
|
|
||||||
case "renderer.js.map":
|
|
||||||
case "vencordDesktopRenderer.js.map":
|
|
||||||
case "preload.js.map":
|
|
||||||
case "patcher.js.map":
|
|
||||||
case "vencordDesktopMain.js.map":
|
|
||||||
cb(join(__dirname, url));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
cb({ statusCode: 403 });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (getSettings().enableReactDevtools)
|
|
||||||
installExt("fmkadmapgofadopljbjfkapdkoienihi")
|
|
||||||
.then(() => console.info("[Vencord] Installed React Developer Tools"))
|
|
||||||
.catch(err => console.error("[Vencord] Failed to install React Developer Tools", err));
|
|
||||||
} catch { }
|
|
||||||
|
|
||||||
|
|
||||||
// Remove CSP
|
|
||||||
type PolicyResult = Record<string, string[]>;
|
|
||||||
|
|
||||||
const parsePolicy = (policy: string): PolicyResult => {
|
|
||||||
const result: PolicyResult = {};
|
|
||||||
policy.split(";").forEach(directive => {
|
|
||||||
const [directiveKey, ...directiveValue] = directive.trim().split(/\s+/g);
|
|
||||||
if (directiveKey && !Object.prototype.hasOwnProperty.call(result, directiveKey)) {
|
|
||||||
result[directiveKey] = directiveValue;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
const stringifyPolicy = (policy: PolicyResult): string =>
|
|
||||||
Object.entries(policy)
|
|
||||||
.filter(([, values]) => values?.length)
|
|
||||||
.map(directive => directive.flat().join(" "))
|
|
||||||
.join("; ");
|
|
||||||
|
|
||||||
function patchCsp(headers: Record<string, string[]>, header: string) {
|
|
||||||
if (header in headers) {
|
|
||||||
const csp = parsePolicy(headers[header][0]);
|
|
||||||
|
|
||||||
for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src", "worker-src"]) {
|
|
||||||
csp[directive] = ["*", "blob:", "data:", "'unsafe-inline'"];
|
|
||||||
}
|
|
||||||
// TODO: Restrict this to only imported packages with fixed version.
|
|
||||||
// Perhaps auto generate with esbuild
|
|
||||||
csp["script-src"] ??= [];
|
|
||||||
csp["script-src"].push("'unsafe-eval'", "https://unpkg.com", "https://cdnjs.cloudflare.com");
|
|
||||||
headers[header] = [stringifyPolicy(csp)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => {
|
|
||||||
if (responseHeaders) {
|
|
||||||
if (resourceType === "mainFrame")
|
|
||||||
patchCsp(responseHeaders, "content-security-policy");
|
|
||||||
|
|
||||||
// Fix hosts that don't properly set the css content type, such as
|
|
||||||
// raw.githubusercontent.com
|
|
||||||
if (resourceType === "stylesheet")
|
|
||||||
responseHeaders["content-type"] = ["text/css"];
|
|
||||||
}
|
|
||||||
cb({ cancel: false, responseHeaders });
|
|
||||||
});
|
|
||||||
|
|
||||||
// assign a noop to onHeadersReceived to prevent other mods from adding their own incompatible ones.
|
|
||||||
// For instance, OpenAsar adds their own that doesn't fix content-type for stylesheets which makes it
|
|
||||||
// impossible to load css from github raw despite our fix above
|
|
||||||
session.defaultSession.webRequest.onHeadersReceived = () => { };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IS_DISCORD_DESKTOP) {
|
|
||||||
require("./patcher");
|
|
||||||
}
|
|
@ -20,8 +20,9 @@ import { onceDefined } from "@utils/onceDefined";
|
|||||||
import electron, { app, BrowserWindowConstructorOptions, Menu } from "electron";
|
import electron, { app, BrowserWindowConstructorOptions, Menu } from "electron";
|
||||||
import { dirname, join } from "path";
|
import { dirname, join } from "path";
|
||||||
|
|
||||||
import { getSettings, initIpc } from "./ipcMain";
|
import { initIpc } from "./ipcMain";
|
||||||
import { IS_VANILLA } from "./utils/constants";
|
import { installExt } from "./ipcMain/extensions";
|
||||||
|
import { readSettings } from "./ipcMain/index";
|
||||||
|
|
||||||
console.log("[Vencord] Starting up...");
|
console.log("[Vencord] Starting up...");
|
||||||
|
|
||||||
@ -40,8 +41,11 @@ require.main!.filename = join(asarPath, discordPkg.main);
|
|||||||
// @ts-ignore Untyped method? Dies from cringe
|
// @ts-ignore Untyped method? Dies from cringe
|
||||||
app.setAppPath(asarPath);
|
app.setAppPath(asarPath);
|
||||||
|
|
||||||
if (!IS_VANILLA) {
|
if (!process.argv.includes("--vanilla")) {
|
||||||
const settings = getSettings();
|
let settings: typeof import("@api/settings").Settings = {} as any;
|
||||||
|
try {
|
||||||
|
settings = JSON.parse(readSettings());
|
||||||
|
} catch { }
|
||||||
|
|
||||||
// Repatch after host updates on Windows
|
// Repatch after host updates on Windows
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
@ -75,21 +79,12 @@ if (!IS_VANILLA) {
|
|||||||
options.webPreferences.sandbox = false;
|
options.webPreferences.sandbox = false;
|
||||||
if (settings.frameless) {
|
if (settings.frameless) {
|
||||||
options.frame = false;
|
options.frame = false;
|
||||||
} else if (process.platform === "win32" && settings.winNativeTitleBar) {
|
|
||||||
delete options.frame;
|
|
||||||
}
|
}
|
||||||
|
if (settings.transparent) {
|
||||||
// This causes electron to freeze / white screen for some people
|
|
||||||
if ((settings as any).transparentUNSAFE_USE_AT_OWN_RISK) {
|
|
||||||
options.transparent = true;
|
options.transparent = true;
|
||||||
options.backgroundColor = "#00000000";
|
options.backgroundColor = "#00000000";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settings.macosTranslucency && process.platform === "darwin") {
|
|
||||||
options.backgroundColor = "#00000000";
|
|
||||||
options.vibrancy = "sidebar";
|
|
||||||
}
|
|
||||||
|
|
||||||
process.env.DISCORD_PRELOAD = original;
|
process.env.DISCORD_PRELOAD = original;
|
||||||
|
|
||||||
super(options);
|
super(options);
|
||||||
@ -111,19 +106,85 @@ if (!IS_VANILLA) {
|
|||||||
BrowserWindow
|
BrowserWindow
|
||||||
};
|
};
|
||||||
|
|
||||||
// Patch appSettings to force enable devtools and optionally disable min size
|
// Patch appSettings to force enable devtools
|
||||||
onceDefined(global, "appSettings", s => {
|
onceDefined(global, "appSettings", s =>
|
||||||
s.set("DANGEROUS_ENABLE_DEVTOOLS_ONLY_ENABLE_IF_YOU_KNOW_WHAT_YOURE_DOING", true);
|
s.set("DANGEROUS_ENABLE_DEVTOOLS_ONLY_ENABLE_IF_YOU_KNOW_WHAT_YOURE_DOING", true)
|
||||||
if (settings.disableMinSize) {
|
);
|
||||||
s.set("MIN_WIDTH", 0);
|
|
||||||
s.set("MIN_HEIGHT", 0);
|
process.env.DATA_DIR = join(app.getPath("userData"), "..", "Vencord");
|
||||||
} else {
|
|
||||||
s.set("MIN_WIDTH", 940);
|
electron.app.whenReady().then(() => {
|
||||||
s.set("MIN_HEIGHT", 500);
|
// Source Maps! Maybe there's a better way but since the renderer is executed
|
||||||
|
// from a string I don't think any other form of sourcemaps would work
|
||||||
|
electron.protocol.registerFileProtocol("vencord", ({ url: unsafeUrl }, cb) => {
|
||||||
|
let url = unsafeUrl.slice("vencord://".length);
|
||||||
|
if (url.endsWith("/")) url = url.slice(0, -1);
|
||||||
|
switch (url) {
|
||||||
|
case "renderer.js.map":
|
||||||
|
case "preload.js.map":
|
||||||
|
case "patcher.js.map": // doubt
|
||||||
|
cb(join(__dirname, url));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
cb({ statusCode: 403 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
process.env.DATA_DIR = join(app.getPath("userData"), "..", "Vencord");
|
try {
|
||||||
|
if (settings?.enableReactDevtools)
|
||||||
|
installExt("fmkadmapgofadopljbjfkapdkoienihi")
|
||||||
|
.then(() => console.info("[Vencord] Installed React Developer Tools"))
|
||||||
|
.catch(err => console.error("[Vencord] Failed to install React Developer Tools", err));
|
||||||
|
} catch { }
|
||||||
|
|
||||||
|
|
||||||
|
// Remove CSP
|
||||||
|
type PolicyResult = Record<string, string[]>;
|
||||||
|
|
||||||
|
const parsePolicy = (policy: string): PolicyResult => {
|
||||||
|
const result: PolicyResult = {};
|
||||||
|
policy.split(";").forEach(directive => {
|
||||||
|
const [directiveKey, ...directiveValue] = directive.trim().split(/\s+/g);
|
||||||
|
if (directiveKey && !Object.prototype.hasOwnProperty.call(result, directiveKey)) {
|
||||||
|
result[directiveKey] = directiveValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
const stringifyPolicy = (policy: PolicyResult): string =>
|
||||||
|
Object.entries(policy)
|
||||||
|
.filter(([, values]) => values?.length)
|
||||||
|
.map(directive => directive.flat().join(" "))
|
||||||
|
.join("; ");
|
||||||
|
|
||||||
|
function patchCsp(headers: Record<string, string[]>, header: string) {
|
||||||
|
if (header in headers) {
|
||||||
|
const csp = parsePolicy(headers[header][0]);
|
||||||
|
|
||||||
|
for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src", "worker-src"]) {
|
||||||
|
csp[directive] = ["*", "blob:", "data:", "'unsafe-inline'"];
|
||||||
|
}
|
||||||
|
// TODO: Restrict this to only imported packages with fixed version.
|
||||||
|
// Perhaps auto generate with esbuild
|
||||||
|
csp["script-src"] ??= [];
|
||||||
|
csp["script-src"].push("'unsafe-eval'", "https://unpkg.com", "https://cdnjs.cloudflare.com");
|
||||||
|
headers[header] = [stringifyPolicy(csp)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
electron.session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => {
|
||||||
|
if (responseHeaders) {
|
||||||
|
if (resourceType === "mainFrame")
|
||||||
|
patchCsp(responseHeaders, "content-security-policy");
|
||||||
|
|
||||||
|
// Fix hosts that don't properly set the css content type, such as
|
||||||
|
// raw.githubusercontent.com
|
||||||
|
if (resourceType === "stylesheet")
|
||||||
|
responseHeaders["content-type"] = ["text/css"];
|
||||||
|
}
|
||||||
|
cb({ cancel: false, responseHeaders });
|
||||||
|
});
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log("[Vencord] Running in vanilla mode. Not loading Vencord");
|
console.log("[Vencord] Running in vanilla mode. Not loading Vencord");
|
||||||
}
|
}
|
@ -32,10 +32,10 @@ export default definePlugin({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: '"7z","ade","adp"',
|
find: "\"github.com\":new RegExp(\"\\\\/releases\\\\S*\\\\/download\"),",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /JSON\.parse\('\[.+?'\)/,
|
match: /const o=JSON.parse\('\[.+?'\)/,
|
||||||
replace: "[]"
|
replace: "const o=[]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { BadgePosition, BadgeUserArgs, ProfileBadge } from "@api/Badges";
|
import { BadgePosition, 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";
|
||||||
@ -24,18 +24,17 @@ import { Heart } from "@components/Heart";
|
|||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import IpcEvents from "@utils/IpcEvents";
|
import IpcEvents from "@utils/IpcEvents";
|
||||||
import Logger from "@utils/Logger";
|
import Logger from "@utils/Logger";
|
||||||
import { Margins } from "@utils/margins";
|
|
||||||
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 } from "@webpack/common";
|
import { Forms, Margins } from "@webpack/common";
|
||||||
|
|
||||||
const CONTRIBUTOR_BADGE = "https://cdn.discordapp.com/attachments/1033680203433660458/1092089947126780035/favicon.png";
|
const CONTRIBUTOR_BADGE = "https://media.discordapp.net/stickers/1026517526106087454.webp";
|
||||||
|
|
||||||
/** List of vencord contributor IDs */
|
/** List of vencord contributor IDs */
|
||||||
const contributorIds: string[] = Object.values(Devs).map(d => d.id.toString());
|
const contributorIds: string[] = Object.values(Devs).map(d => d.id.toString());
|
||||||
|
|
||||||
const ContributorBadge: ProfileBadge = {
|
const ContributorBadge: ProfileBadge = {
|
||||||
description: "Vencord Contributor",
|
tooltip: "Vencord Contributor",
|
||||||
image: CONTRIBUTOR_BADGE,
|
image: CONTRIBUTOR_BADGE,
|
||||||
position: BadgePosition.START,
|
position: BadgePosition.START,
|
||||||
props: {
|
props: {
|
||||||
@ -45,23 +44,23 @@ const ContributorBadge: ProfileBadge = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
shouldShow: ({ user }) => contributorIds.includes(user.id),
|
shouldShow: ({ user }) => contributorIds.includes(user.id),
|
||||||
link: "https://github.com/Vendicated/Vencord"
|
onClick: () => VencordNative.ipc.invoke(IpcEvents.OPEN_EXTERNAL, "https://github.com/Vendicated/Vencord")
|
||||||
};
|
};
|
||||||
|
|
||||||
const DonorBadges = {} as Record<string, Pick<ProfileBadge, "image" | "description">>;
|
const DonorBadges = {} as Record<string, Pick<ProfileBadge, "image" | "tooltip">>;
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "BadgeAPI",
|
name: "BadgeAPI",
|
||||||
description: "API to add badges to users.",
|
description: "API to add badges to users.",
|
||||||
authors: [Devs.Megu, Devs.Ven, Devs.TheSun],
|
authors: [Devs.Megu],
|
||||||
required: true,
|
required: true,
|
||||||
patches: [
|
patches: [
|
||||||
/* Patch the badges array */
|
/* Patch the badges array */
|
||||||
{
|
{
|
||||||
find: "Messages.ACTIVE_DEVELOPER_BADGE_TOOLTIP",
|
find: "PREMIUM_GUILD_SUBSCRIPTION_TOOLTIP.format({date:",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<=void 0:)\i.getBadges\(\)/,
|
match: /&&((\w{1,3})\.push\({tooltip:\w{1,3}\.\w{1,3}\.Messages\.PREMIUM_GUILD_SUBSCRIPTION_TOOLTIP\.format.+?;)(?:return\s\w{1,3};?})/,
|
||||||
replace: "Vencord.Api.Badges._getBadges(arguments[0]).concat($&??[])",
|
replace: (_, m, badgeArray) => `&&${m} return Vencord.Api.Badges.inject(${badgeArray}, arguments[0]);}`,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
/* Patch the badge list component on user profiles */
|
/* Patch the badge list component on user profiles */
|
||||||
@ -69,28 +68,21 @@ export default definePlugin({
|
|||||||
find: "Messages.PROFILE_USER_BADGES,role:",
|
find: "Messages.PROFILE_USER_BADGES,role:",
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
// alt: "", aria-hidden: false, src: originalSrc
|
match: /src:(\w{1,3})\[(\w{1,3})\.key\],/,
|
||||||
match: /alt:" ","aria-hidden":!0,src:(?=.{0,10}\b(\i)\.(?:icon|key))/g,
|
// <img src={badge.image ?? imageMap[badge.key]} {...badge.props} />
|
||||||
// ...badge.props, ..., src: badge.image ?? ...
|
replace: (_, imageMap, badge) => `src: ${badge}.image ?? ${imageMap}[${badge}.key], ...${badge}.props,`
|
||||||
replace: "...$1.props,$& $1.image??"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /children:function(?<=(\i)\.(?:tooltip|description),spacing:\d.+?)/g,
|
match: /spacing:(\d{1,2}),children:(.{1,40}(\i)\.jsx.+?(\i)\.onClick.+?\)})},/,
|
||||||
replace: "children:$1.component ? () => $self.renderBadgeComponent($1) : function"
|
// if the badge provides it's own component, render that instead of an image
|
||||||
},
|
// the badge also includes info about the user that has it (type BadgeUserArgs), which is why it's passed as props
|
||||||
{
|
replace: (_, s, origBadgeComponent, React, badge) =>
|
||||||
match: /onClick:function(?=.{0,200}href:(\i)\.link)/,
|
`spacing:${s},children:${badge}.component ? () => (0,${React}.jsx)(${badge}.component, { ...${badge} }) : ${origBadgeComponent}},`
|
||||||
replace: "onClick:$1.onClick??function"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
renderBadgeComponent: ErrorBoundary.wrap((badge: ProfileBadge & BadgeUserArgs) => {
|
|
||||||
const Component = badge.component!;
|
|
||||||
return <Component {...badge} />;
|
|
||||||
}, { noop: true }),
|
|
||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
Vencord.Api.Badges.addBadge(ContributorBadge);
|
Vencord.Api.Badges.addBadge(ContributorBadge);
|
||||||
const badges = await fetch("https://gist.githubusercontent.com/Vendicated/51a3dd775f6920429ec6e9b735ca7f01/raw/badges.csv").then(r => r.text());
|
const badges = await fetch("https://gist.githubusercontent.com/Vendicated/51a3dd775f6920429ec6e9b735ca7f01/raw/badges.csv").then(r => r.text());
|
||||||
@ -100,15 +92,15 @@ export default definePlugin({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const [id, description, image] = line.split(",");
|
const [id, tooltip, image] = line.split(",");
|
||||||
DonorBadges[id] = { image, description };
|
DonorBadges[id] = { image, tooltip };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getDonorBadge(userId: string) {
|
addDonorBadge(badges: ProfileBadge[], userId: string) {
|
||||||
const badge = DonorBadges[userId];
|
const badge = DonorBadges[userId];
|
||||||
if (badge) {
|
if (badge) {
|
||||||
return {
|
badges.unshift({
|
||||||
...badge,
|
...badge,
|
||||||
position: BadgePosition.START,
|
position: BadgePosition.START,
|
||||||
props: {
|
props: {
|
||||||
@ -158,7 +150,7 @@ export default definePlugin({
|
|||||||
<Forms.FormText>
|
<Forms.FormText>
|
||||||
This Badge is a special perk for Vencord Donors
|
This Badge is a special perk for Vencord Donors
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
<Forms.FormText className={Margins.top20}>
|
<Forms.FormText className={Margins.marginTop20}>
|
||||||
Please consider supporting the development of Vencord by becoming a donor. It would mean a lot!!
|
Please consider supporting the development of Vencord by becoming a donor. It would mean a lot!!
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
</div>
|
</div>
|
||||||
@ -172,7 +164,7 @@ export default definePlugin({
|
|||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
82
src/plugins/apiMenuItemDeobfuscator.ts
Normal file
82
src/plugins/apiMenuItemDeobfuscator.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { migratePluginSettings } from "@api/settings";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import definePlugin from "@utils/types";
|
||||||
|
|
||||||
|
// duplicate values have multiple branches with different types. Just include all to be safe
|
||||||
|
const nameMap = {
|
||||||
|
radio: "MenuRadioItem",
|
||||||
|
separator: "MenuSeparator",
|
||||||
|
checkbox: "MenuCheckboxItem",
|
||||||
|
groupstart: "MenuGroup",
|
||||||
|
|
||||||
|
control: "MenuControlItem",
|
||||||
|
compositecontrol: "MenuControlItem",
|
||||||
|
|
||||||
|
item: "MenuItem",
|
||||||
|
customitem: "MenuItem",
|
||||||
|
};
|
||||||
|
|
||||||
|
migratePluginSettings("MenuItemDeobfuscatorAPI", "MenuItemDeobfuscatorApi");
|
||||||
|
export default definePlugin({
|
||||||
|
name: "MenuItemDeobfuscatorAPI",
|
||||||
|
description: "Deobfuscates Discord's Menu Item module",
|
||||||
|
authors: [Devs.Ven],
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: '"Menu API',
|
||||||
|
replacement: {
|
||||||
|
match: /function.{0,80}type===(.{1,3})\..{1,3}\).{0,50}navigable:.+?Menu API/s,
|
||||||
|
replace: (m, mod) => {
|
||||||
|
let nicenNames = "";
|
||||||
|
const redefines = [] as string[];
|
||||||
|
// if (t.type === m.MenuItem)
|
||||||
|
const typeCheckRe = /\(.{1,3}\.type===(.{1,5})\)/g;
|
||||||
|
// push({type:"item"})
|
||||||
|
const pushTypeRe = /type:"(\w+)"/g;
|
||||||
|
|
||||||
|
let typeMatch: RegExpExecArray | null;
|
||||||
|
// for each if (t.type === ...)
|
||||||
|
while ((typeMatch = typeCheckRe.exec(m)) !== null) {
|
||||||
|
// extract the current menu item
|
||||||
|
const item = typeMatch[1];
|
||||||
|
// Set the starting index of the second regex to that of the first to start
|
||||||
|
// matching from after the if
|
||||||
|
pushTypeRe.lastIndex = typeCheckRe.lastIndex;
|
||||||
|
// extract the first type: "..."
|
||||||
|
const type = pushTypeRe.exec(m)?.[1];
|
||||||
|
if (type && type in nameMap) {
|
||||||
|
const name = nameMap[type];
|
||||||
|
nicenNames += `Object.defineProperty(${item},"name",{value:"${name}"});`;
|
||||||
|
redefines.push(`${name}:${item}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (redefines.length < 6) {
|
||||||
|
console.warn("[ApiMenuItemDeobfuscator] Expected to at least remap 6 items, only remapped", redefines.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge all our redefines with the actual module
|
||||||
|
return `${nicenNames}Object.assign(${mod},{${redefines.join(",")}});${m}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
@ -22,22 +22,22 @@ import definePlugin from "@utils/types";
|
|||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "MessageEventsAPI",
|
name: "MessageEventsAPI",
|
||||||
description: "Api required by anything using message events.",
|
description: "Api required by anything using message events.",
|
||||||
authors: [Devs.Arjix, Devs.hunt],
|
authors: [Devs.Arjix],
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: '"MessageActionCreators"',
|
find: "sendMessage:function",
|
||||||
replacement: [{
|
replacement: [{
|
||||||
match: /_sendMessage:(function\([^)]+\)){/,
|
match: /(?<=_sendMessage:function\([^)]+\)){/,
|
||||||
replace: "_sendMessage:async $1{if(await Vencord.Api.MessageEvents._handlePreSend(...arguments))return;"
|
replace: "{if(Vencord.Api.MessageEvents._handlePreSend(...arguments)){return;};"
|
||||||
}, {
|
}, {
|
||||||
match: /\beditMessage:(function\([^)]+\)){/,
|
match: /(?<=\beditMessage:function\([^)]+\)){/,
|
||||||
replace: "editMessage:async $1{await Vencord.Api.MessageEvents._handlePreEdit(...arguments);"
|
replace: "{Vencord.Api.MessageEvents._handlePreEdit(...arguments);"
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: '("interactionUsernameProfile',
|
find: '("interactionUsernameProfile',
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /var \i=(\i)\.id,\i=(\i)\.id;return \i\.useCallback\(\(?function\((\i)\){/,
|
match: /var \w=(\w)\.id,\w=(\w)\.id;return .{1,2}\.useCallback\(\(?function\((.{1,2})\){/,
|
||||||
replace: (m, message, channel, event) =>
|
replace: (m, message, channel, event) =>
|
||||||
// the message param is shadowed by the event param, so need to alias them
|
// the message param is shadowed by the event param, so need to alias them
|
||||||
`var _msg=${message},_chan=${channel};${m}Vencord.Api.MessageEvents._handleClick(_msg, _chan, ${event});`
|
`var _msg=${message},_chan=${channel};${m}Vencord.Api.MessageEvents._handleClick(_msg, _chan, ${event});`
|
||||||
|
@ -22,16 +22,16 @@ import definePlugin from "@utils/types";
|
|||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "MessagePopoverAPI",
|
name: "MessagePopoverAPI",
|
||||||
description: "API to add buttons to message popovers.",
|
description: "API to add buttons to message popovers.",
|
||||||
authors: [Devs.KingFish, Devs.Ven, Devs.Nuckyz],
|
authors: [Devs.KingFish, Devs.Ven],
|
||||||
patches: [{
|
patches: [{
|
||||||
find: "Messages.MESSAGE_UTILITIES_A11Y_LABEL",
|
find: "Messages.MESSAGE_UTILITIES_A11Y_LABEL",
|
||||||
replacement: {
|
replacement: {
|
||||||
// foo && !bar ? createElement(reactionStuffs)... createElement(blah,...makeElement(reply-other))
|
// foo && !bar ? createElement(blah,...makeElement(addReactionData))
|
||||||
match: /\i&&!\i\?\(0,\i\.jsxs?\)\(.{0,200}renderEmojiPicker:.{0,500}\?(\i)\(\{key:"reply-other"/,
|
match: /(\i&&!\i)\?\(0,\i\.jsxs?\)\(.{0,20}renderPopout:.{0,300}?(\i)\(.{3,20}\{key:"add-reaction".+?\}/,
|
||||||
replace: (m, makeElement) => {
|
replace: (m, bools, makeElement) => {
|
||||||
const msg = m.match(/message:(.{1,3}),/)?.[1];
|
const msg = m.match(/message:(.{1,3}),/)?.[1];
|
||||||
if (!msg) throw new Error("Could not find message variable");
|
if (!msg) throw new Error("Could not find message variable");
|
||||||
return `...Vencord.Api.MessagePopover._buildPopoverElements(${msg},${makeElement}),${m}`;
|
return `...(${bools}?Vencord.Api.MessagePopover._buildPopoverElements(${msg},${makeElement}):[]),${m}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
|
@ -29,12 +29,13 @@ export default definePlugin({
|
|||||||
find: 'displayName="NoticeStore"',
|
find: 'displayName="NoticeStore"',
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /(?=;\i=null;.{0,70}getPremiumSubscription)/g,
|
match: /;.{1,2}=null;.{0,70}getPremiumSubscription/g,
|
||||||
replace: ";if(Vencord.Api.Notices.currentNotice)return false"
|
replace:
|
||||||
|
";if(Vencord.Api.Notices.currentNotice)return false$&"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /(?<=,NOTICE_DISMISS:function\(\i\){)(?=if\(null==(\i)\))/,
|
match: /(?<=,NOTICE_DISMISS:function\(\i\){)(?=if\(null==(\i)\))/,
|
||||||
replace: (_, notice) => `if(${notice}.id=="VencordNotice")return(${notice}=null,Vencord.Api.Notices.nextNotice(),true);`
|
replace: 'if($1?.id=="VencordNotice")return($1=null,Vencord.Api.Notices.nextNotice(),true);'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -1,38 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Devs } from "@utils/constants";
|
|
||||||
import definePlugin from "@utils/types";
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "SettingsStoreAPI",
|
|
||||||
description: "Patches Discord's SettingsStores to expose their group and name",
|
|
||||||
authors: [Devs.Nuckyz],
|
|
||||||
|
|
||||||
patches: [
|
|
||||||
{
|
|
||||||
find: '"textAndImages","renderSpoilers"',
|
|
||||||
replacement: [
|
|
||||||
{
|
|
||||||
match: /(?<=INFREQUENT_USER_ACTION.{0,20}),useSetting:function/,
|
|
||||||
replace: ",settingsStoreApiGroup:arguments[0],settingsStoreApiName:arguments[1]$&"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
@ -48,6 +48,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],
|
||||||
|
target: "WEB",
|
||||||
|
|
||||||
settingsAboutComponent: () => (
|
settingsAboutComponent: () => (
|
||||||
<>
|
<>
|
||||||
@ -59,9 +60,6 @@ export default definePlugin({
|
|||||||
),
|
),
|
||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
// ArmCord comes with its own arRPC implementation, so this plugin just confuses users
|
|
||||||
if ("armcord" in window) return;
|
|
||||||
|
|
||||||
if (ws) ws.close();
|
if (ws) ws.close();
|
||||||
ws = new WebSocket("ws://127.0.0.1:1337"); // try to open WebSocket
|
ws = new WebSocket("ws://127.0.0.1:1337"); // try to open WebSocket
|
||||||
|
|
@ -1,84 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2023 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Settings } from "@api/settings";
|
|
||||||
import { classNameFactory } from "@api/Styles";
|
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
|
||||||
import { findByPropsLazy, findStoreLazy } from "@webpack";
|
|
||||||
import { i18n, React, useStateFromStores } from "@webpack/common";
|
|
||||||
|
|
||||||
const cl = classNameFactory("vc-bf-");
|
|
||||||
const classes = findByPropsLazy("sidebar", "guilds");
|
|
||||||
|
|
||||||
const Animations = findByPropsLazy("a", "animated", "useTransition");
|
|
||||||
const ChannelRTCStore = findStoreLazy("ChannelRTCStore");
|
|
||||||
const ExpandedGuildFolderStore = findStoreLazy("ExpandedGuildFolderStore");
|
|
||||||
|
|
||||||
function Guilds(props: {
|
|
||||||
className: string;
|
|
||||||
bfGuildFolders: any[];
|
|
||||||
}) {
|
|
||||||
// @ts-expect-error
|
|
||||||
const res = Vencord.Plugins.plugins.BetterFolders.Guilds(props);
|
|
||||||
|
|
||||||
const scrollerProps = res.props.children?.props?.children?.[1]?.props;
|
|
||||||
if (scrollerProps?.children) {
|
|
||||||
const servers = scrollerProps.children.find(c => c?.props?.["aria-label"] === i18n.Messages.SERVERS);
|
|
||||||
if (servers) scrollerProps.children = servers;
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ErrorBoundary.wrap(() => {
|
|
||||||
const expandedFolders = useStateFromStores([ExpandedGuildFolderStore], () => ExpandedGuildFolderStore.getExpandedFolders());
|
|
||||||
const fullscreen = useStateFromStores([ChannelRTCStore], () => ChannelRTCStore.isFullscreenInContext());
|
|
||||||
|
|
||||||
const guilds = document.querySelector(`.${classes.guilds}`);
|
|
||||||
|
|
||||||
const visible = !!expandedFolders.size;
|
|
||||||
const className = cl("folder-sidebar", { fullscreen });
|
|
||||||
|
|
||||||
const Sidebar = (
|
|
||||||
<Guilds
|
|
||||||
className={classes.guilds}
|
|
||||||
bfGuildFolders={Array.from(expandedFolders)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!guilds || !Settings.plugins.BetterFolders.sidebarAnim)
|
|
||||||
return visible
|
|
||||||
? <div className={className}>{Sidebar}</div>
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Animations.Transition
|
|
||||||
items={visible}
|
|
||||||
from={{ width: 0 }}
|
|
||||||
enter={{ width: guilds.getBoundingClientRect().width }}
|
|
||||||
leave={{ width: 0 }}
|
|
||||||
config={{ duration: 200 }}
|
|
||||||
>
|
|
||||||
{(style, show) => show && (
|
|
||||||
<Animations.animated.div style={style} className={className}>
|
|
||||||
{Sidebar}
|
|
||||||
</Animations.animated.div>
|
|
||||||
)}
|
|
||||||
</Animations.Transition>
|
|
||||||
);
|
|
||||||
}, { noop: true });
|
|
@ -1,17 +0,0 @@
|
|||||||
.vc-bf-folder-sidebar [class*="wrapper-"] > [class*="listItem-"]:first-of-type,
|
|
||||||
.vc-bf-folder-sidebar [class*="unreadMentionsIndicator"] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-bf-folder-sidebar [class*="expandedFolderBackground-"] {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-bf-folder-sidebar {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-bf-fullscreen {
|
|
||||||
width: 0 !important;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
@ -1,177 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2023 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import "./betterFolders.css";
|
|
||||||
|
|
||||||
import { definePluginSettings } from "@api/settings";
|
|
||||||
import { Devs } from "@utils/constants";
|
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
|
||||||
import { findByPropsLazy, findLazy, findStoreLazy } from "@webpack";
|
|
||||||
import { FluxDispatcher } from "@webpack/common";
|
|
||||||
|
|
||||||
import FolderSideBar from "./FolderSideBar";
|
|
||||||
|
|
||||||
const GuildsTree = findLazy(m => m.prototype?.convertToFolder);
|
|
||||||
const GuildFolderStore = findStoreLazy("SortedGuildStore");
|
|
||||||
const ExpandedFolderStore = findStoreLazy("ExpandedGuildFolderStore");
|
|
||||||
const FolderUtils = findByPropsLazy("move", "toggleGuildFolderExpand");
|
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
|
||||||
sidebar: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
description: "Display servers from folder on dedicated sidebar",
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
sidebarAnim: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
description: "Animate opening the folder sidebar",
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
closeAllFolders: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
description: "Close all folders when selecting a server not in a folder",
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
closeAllHomeButton: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
description: "Close all folders when clicking on the home button",
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
closeOthers: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
description: "Close other folders when opening a folder",
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
forceOpen: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
description: "Force a folder to open when switching to a server of that folder",
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "BetterFolders",
|
|
||||||
description: "Shows server folders on dedicated sidebar and adds folder related improvements",
|
|
||||||
authors: [Devs.juby, Devs.AutumnVN],
|
|
||||||
patches: [
|
|
||||||
{
|
|
||||||
find: '("guildsnav")',
|
|
||||||
predicate: () => settings.store.sidebar,
|
|
||||||
replacement: [
|
|
||||||
{
|
|
||||||
match: /(\i)\(\){return \i\(\(0,\i\.jsx\)\("div",{className:\i\(\)\.guildSeparator}\)\)}/,
|
|
||||||
replace: "$&$self.Separator=$1;"
|
|
||||||
},
|
|
||||||
|
|
||||||
// Folder component patch
|
|
||||||
{
|
|
||||||
match: /\i\(\(function\(\i,\i,\i\){var \i=\i\.key;return.+\(\i\)},\i\)}\)\)/,
|
|
||||||
replace: "arguments[0].bfHideServers?null:$&"
|
|
||||||
},
|
|
||||||
|
|
||||||
// BEGIN Guilds component patch
|
|
||||||
{
|
|
||||||
match: /(\i)\.themeOverride,(.{15,25}\(function\(\){var \i=)(\i\.\i\.getGuildsTree\(\))/,
|
|
||||||
replace: "$1.themeOverride,bfPatch=$1.bfGuildFolders,$2bfPatch?$self.getGuildsTree(bfPatch,$3):$3"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
match: /return(\(0,\i\.jsx\))(\(\i,{)(folderNode:\i,setNodeRef:\i\.setNodeRef,draggable:!0,.+},\i\.id\));case/,
|
|
||||||
replace: "var bfHideServers=typeof bfPatch==='undefined',folder=$1$2bfHideServers,$3;return !bfHideServers&&arguments[1]?[$1($self.Separator,{}),folder]:folder;case"
|
|
||||||
},
|
|
||||||
// END
|
|
||||||
|
|
||||||
{
|
|
||||||
match: /\("guildsnav"\);return\(0,\i\.jsx\)\(.{1,6},{navigator:\i,children:\(0,\i\.jsx\)\(/,
|
|
||||||
replace: "$&$self.Guilds="
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: "APPLICATION_LIBRARY,render",
|
|
||||||
predicate: () => settings.store.sidebar,
|
|
||||||
replacement: {
|
|
||||||
match: /(\(0,\i\.jsx\))\(\i\..,{className:\i\(\)\.guilds,themeOverride:\i}\)/,
|
|
||||||
replace: "$&,$1($self.FolderSideBar,{})"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: '("guildsnav")',
|
|
||||||
predicate: () => settings.store.closeAllHomeButton,
|
|
||||||
replacement: {
|
|
||||||
match: ",onClick:function(){if(!__OVERLAY__){",
|
|
||||||
replace: "$&$self.closeFolders();"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
settings,
|
|
||||||
|
|
||||||
start() {
|
|
||||||
const getGuildFolder = (id: string) => GuildFolderStore.getGuildFolders().find(f => f.guildIds.includes(id));
|
|
||||||
|
|
||||||
FluxDispatcher.subscribe("CHANNEL_SELECT", this.onSwitch = data => {
|
|
||||||
if (!settings.store.closeAllFolders && !settings.store.forceOpen)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (this.lastGuildId !== data.guildId) {
|
|
||||||
this.lastGuildId = data.guildId;
|
|
||||||
|
|
||||||
const guildFolder = getGuildFolder(data.guildId);
|
|
||||||
if (guildFolder?.folderId) {
|
|
||||||
if (settings.store.forceOpen && !ExpandedFolderStore.isFolderExpanded(guildFolder.folderId))
|
|
||||||
FolderUtils.toggleGuildFolderExpand(guildFolder.folderId);
|
|
||||||
} else if (settings.store.closeAllFolders)
|
|
||||||
this.closeFolders();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
FluxDispatcher.subscribe("TOGGLE_GUILD_FOLDER_EXPAND", this.onToggleFolder = e => {
|
|
||||||
if (settings.store.closeOthers && !this.dispatching)
|
|
||||||
FluxDispatcher.wait(() => {
|
|
||||||
const expandedFolders = ExpandedFolderStore.getExpandedFolders();
|
|
||||||
if (expandedFolders.size > 1) {
|
|
||||||
this.dispatching = true;
|
|
||||||
|
|
||||||
for (const id of expandedFolders) if (id !== e.folderId)
|
|
||||||
FolderUtils.toggleGuildFolderExpand(id);
|
|
||||||
|
|
||||||
this.dispatching = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
FluxDispatcher.unsubscribe("CHANNEL_SELECT", this.onSwitch);
|
|
||||||
FluxDispatcher.unsubscribe("TOGGLE_GUILD_FOLDER_EXPAND", this.onToggleFolder);
|
|
||||||
},
|
|
||||||
|
|
||||||
FolderSideBar,
|
|
||||||
|
|
||||||
getGuildsTree(folders, oldTree) {
|
|
||||||
const tree = new GuildsTree();
|
|
||||||
tree.root.children = oldTree.root.children.filter(e => folders.includes(e.id));
|
|
||||||
tree.nodes = folders.map(id => oldTree.nodes[id]);
|
|
||||||
return tree;
|
|
||||||
},
|
|
||||||
|
|
||||||
closeFolders() {
|
|
||||||
for (const id of ExpandedFolderStore.getExpandedFolders())
|
|
||||||
FolderUtils.toggleGuildFolderExpand(id);
|
|
||||||
},
|
|
||||||
});
|
|
@ -37,14 +37,14 @@ export default definePlugin({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: '"dot"===',
|
find: '"username"===',
|
||||||
all: true,
|
all: true,
|
||||||
predicate: () => Settings.plugins.BetterRoleDot.bothStyles,
|
predicate: () => Settings.plugins.BetterRoleDot.bothStyles,
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /"(?:username|dot)"===\i(?!\.\i)/g,
|
match: /"(?:username|dot)"===\w(?!\.\w)/g,
|
||||||
replace: "true",
|
replace: "true",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
options: {
|
options: {
|
||||||
|
@ -27,16 +27,11 @@ export default definePlugin({
|
|||||||
{
|
{
|
||||||
find: "Masks.STATUS_ONLINE",
|
find: "Masks.STATUS_ONLINE",
|
||||||
replacement: {
|
replacement: {
|
||||||
|
// we can use global replacement here - these are specific to the status icons and are used nowhere else,
|
||||||
|
// so it keeps the patch and plugin small and simple
|
||||||
match: /Masks\.STATUS_(?:IDLE|DND|STREAMING|OFFLINE)/g,
|
match: /Masks\.STATUS_(?:IDLE|DND|STREAMING|OFFLINE)/g,
|
||||||
replace: "Masks.STATUS_ONLINE"
|
replace: "Masks.STATUS_ONLINE"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
find: ".AVATAR_STATUS_MOBILE_16;",
|
|
||||||
replacement: {
|
|
||||||
match: /(\.fromIsMobile,.+?)\i.status/,
|
|
||||||
replace: (_, rest) => `${rest}"online"`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
@ -17,13 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { relaunch } from "@utils/native";
|
|
||||||
import { canonicalizeMatch, canonicalizeReplace, canonicalizeReplacement } from "@utils/patches";
|
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import * as Webpack from "@webpack";
|
|
||||||
import { extract, filters, findAll, search } from "@webpack";
|
|
||||||
import { React, ReactDOM } from "@webpack/common";
|
|
||||||
import type { ComponentType } from "react";
|
|
||||||
|
|
||||||
const WEB_ONLY = (f: string) => () => {
|
const WEB_ONLY = (f: string) => () => {
|
||||||
throw new Error(`'${f}' is Discord Desktop only.`);
|
throw new Error(`'${f}' is Discord Desktop only.`);
|
||||||
@ -35,65 +29,23 @@ export default definePlugin({
|
|||||||
authors: [Devs.Ven],
|
authors: [Devs.Ven],
|
||||||
|
|
||||||
getShortcuts() {
|
getShortcuts() {
|
||||||
function newFindWrapper(filterFactory: (...props: any[]) => Webpack.FilterFn) {
|
|
||||||
const cache = new Map<string, unknown>();
|
|
||||||
|
|
||||||
return function (...filterProps: unknown[]) {
|
|
||||||
const cacheKey = String(filterProps);
|
|
||||||
if (cache.has(cacheKey)) return cache.get(cacheKey);
|
|
||||||
|
|
||||||
const matches = findAll(filterFactory(...filterProps));
|
|
||||||
|
|
||||||
const result = (() => {
|
|
||||||
switch (matches.length) {
|
|
||||||
case 0: return null;
|
|
||||||
case 1: return matches[0];
|
|
||||||
default:
|
|
||||||
const uniqueMatches = [...new Set(matches)];
|
|
||||||
if (uniqueMatches.length > 1)
|
|
||||||
console.warn(`Warning: This filter matches ${matches.length} modules. Make it more specific!\n`, uniqueMatches);
|
|
||||||
|
|
||||||
return matches[0];
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
if (result && cacheKey) cache.set(cacheKey, result);
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let fakeRenderWin: WeakRef<Window> | undefined;
|
|
||||||
return {
|
return {
|
||||||
|
toClip: IS_WEB ? WEB_ONLY("toClip") : window.DiscordNative.clipboard.copy,
|
||||||
|
fromClip: IS_WEB ? WEB_ONLY("fromClip") : window.DiscordNative.clipboard.read,
|
||||||
wp: Vencord.Webpack,
|
wp: Vencord.Webpack,
|
||||||
wpc: Webpack.wreq.c,
|
wpc: Vencord.Webpack.wreq.c,
|
||||||
wreq: Webpack.wreq,
|
wreq: Vencord.Webpack.wreq,
|
||||||
wpsearch: search,
|
wpsearch: Vencord.Webpack.search,
|
||||||
wpex: extract,
|
wpex: Vencord.Webpack.extract,
|
||||||
wpexs: (code: string) => Vencord.Webpack.extract(Vencord.Webpack.findModuleId(code)!),
|
wpexs: (code: string) => Vencord.Webpack.extract(Vencord.Webpack.findModuleId(code)!),
|
||||||
find: newFindWrapper(f => f),
|
findByProps: Vencord.Webpack.findByProps,
|
||||||
findAll,
|
find: Vencord.Webpack.find,
|
||||||
findByProps: newFindWrapper(filters.byProps),
|
Plugins: Vencord.Plugins,
|
||||||
findAllByProps: (...props: string[]) => findAll(filters.byProps(...props)),
|
React: Vencord.Webpack.Common.React,
|
||||||
findByCode: newFindWrapper(filters.byCode),
|
|
||||||
findAllByCode: (code: string) => findAll(filters.byCode(code)),
|
|
||||||
findStore: newFindWrapper(filters.byStoreName),
|
|
||||||
PluginsApi: Vencord.Plugins,
|
|
||||||
plugins: Vencord.Plugins.plugins,
|
|
||||||
React,
|
|
||||||
Settings: Vencord.Settings,
|
Settings: Vencord.Settings,
|
||||||
Api: Vencord.Api,
|
Api: Vencord.Api,
|
||||||
reload: () => location.reload(),
|
reload: () => location.reload(),
|
||||||
restart: IS_WEB ? WEB_ONLY("restart") : relaunch,
|
restart: IS_WEB ? WEB_ONLY("restart") : window.DiscordNative.app.relaunch
|
||||||
canonicalizeMatch,
|
|
||||||
canonicalizeReplace,
|
|
||||||
canonicalizeReplacement,
|
|
||||||
fakeRender: (component: ComponentType, props: any) => {
|
|
||||||
const prevWin = fakeRenderWin?.deref();
|
|
||||||
const win = prevWin?.closed === false ? prevWin : window.open("about:blank", "Fake Render", "popup,width=500,height=500")!;
|
|
||||||
fakeRenderWin = new WeakRef(win);
|
|
||||||
win.focus();
|
|
||||||
|
|
||||||
ReactDOM.render(React.createElement(component, props), win.document.body);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
105
src/plugins/corruptMp4s.ts
Normal file
105
src/plugins/corruptMp4s.ts
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ApplicationCommandOptionType, sendBotMessage } from "@api/Commands";
|
||||||
|
import { findOption } from "@api/Commands/commandHelpers";
|
||||||
|
import { ApplicationCommandInputType } from "@api/Commands/types";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import definePlugin from "@utils/types";
|
||||||
|
import { findByCode, findByProps } from "@webpack";
|
||||||
|
|
||||||
|
const DRAFT_TYPE = 0;
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "CorruptMp4s",
|
||||||
|
description: "Create corrupt mp4s with extremely high or negative duration",
|
||||||
|
authors: [Devs.Ven],
|
||||||
|
dependencies: ["CommandsAPI"],
|
||||||
|
commands: [{
|
||||||
|
name: "corrupt",
|
||||||
|
description: "Create a corrupt mp4 with extremely high or negative duration",
|
||||||
|
inputType: ApplicationCommandInputType.BUILT_IN,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: "mp4",
|
||||||
|
description: "the video to corrupt",
|
||||||
|
type: ApplicationCommandOptionType.ATTACHMENT,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "kind",
|
||||||
|
description: "the kind of corruption",
|
||||||
|
type: ApplicationCommandOptionType.STRING,
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
name: "infinite",
|
||||||
|
value: "infinite",
|
||||||
|
label: "Very high duration"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "negative",
|
||||||
|
value: "negative",
|
||||||
|
label: "Negative duration"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
execute: async (args, ctx) => {
|
||||||
|
const UploadStore = findByProps("getUploads");
|
||||||
|
const upload = UploadStore.getUploads(ctx.channel.id, DRAFT_TYPE)[0];
|
||||||
|
|
||||||
|
const video = upload?.item?.file as File | undefined;
|
||||||
|
|
||||||
|
if (video?.type !== "video/mp4")
|
||||||
|
return void sendBotMessage(ctx.channel.id, {
|
||||||
|
content: "Please upload a mp4 file"
|
||||||
|
});
|
||||||
|
|
||||||
|
const corruption = findOption<string>(args, "kind", "infinite");
|
||||||
|
|
||||||
|
const buf = new Uint8Array(await video.arrayBuffer());
|
||||||
|
let found = false;
|
||||||
|
|
||||||
|
// adapted from https://github.com/GeopJr/exorcism/blob/c9a12d77ccbcb49c987b385eafae250906efc297/src/App.svelte#L41-L48
|
||||||
|
for (let i = 0; i < buf.length; i++) {
|
||||||
|
if (buf[i] === 0x6d && buf[i + 1] === 0x76 && buf[i + 2] === 0x68 && buf[i + 3] === 0x64) {
|
||||||
|
let start = i + 18;
|
||||||
|
buf[start++] = 0x00;
|
||||||
|
buf[start++] = 0x01;
|
||||||
|
buf[start++] = corruption === "negative" ? 0xff : 0x7f;
|
||||||
|
buf[start++] = 0xff;
|
||||||
|
buf[start++] = 0xff;
|
||||||
|
buf[start++] = corruption === "negative" ? 0xf0 : 0xff;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
return void sendBotMessage(ctx.channel.id, {
|
||||||
|
content: "Could not find signature. Is this even a mp4?"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const newName = video.name.replace(/\.mp4$/i, ".corrupt.mp4");
|
||||||
|
const promptToUpload = findByCode("UPLOAD_FILE_LIMIT_ERROR");
|
||||||
|
const file = new File([buf], newName, { type: "video/mp4" });
|
||||||
|
setTimeout(() => promptToUpload([file], ctx.channel, DRAFT_TYPE), 10);
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
});
|
@ -1,165 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { showNotification } from "@api/Notifications";
|
|
||||||
import { definePluginSettings } from "@api/settings";
|
|
||||||
import { Devs } from "@utils/constants";
|
|
||||||
import Logger from "@utils/Logger";
|
|
||||||
import { closeAllModals } from "@utils/modal";
|
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
|
||||||
import { maybePromptToUpdate } from "@utils/updater";
|
|
||||||
import { FluxDispatcher, NavigationRouter } from "@webpack/common";
|
|
||||||
import type { ReactElement } from "react";
|
|
||||||
|
|
||||||
const CrashHandlerLogger = new Logger("CrashHandler");
|
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
|
||||||
attemptToPreventCrashes: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
description: "Whether to attempt to prevent Discord crashes.",
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
attemptToNavigateToHome: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
description: "Whether to attempt to navigate to the home when preventing Discord crashes.",
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let crashCount: number = 0;
|
|
||||||
let lastCrashTimestamp: number = 0;
|
|
||||||
let shouldAttemptNextHandle = false;
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "CrashHandler",
|
|
||||||
description: "Utility plugin for handling and possibly recovering from Crashes without a restart",
|
|
||||||
authors: [Devs.Nuckyz],
|
|
||||||
enabledByDefault: true,
|
|
||||||
|
|
||||||
popAllModals: undefined as (() => void) | undefined,
|
|
||||||
|
|
||||||
settings,
|
|
||||||
|
|
||||||
patches: [
|
|
||||||
{
|
|
||||||
find: ".Messages.ERRORS_UNEXPECTED_CRASH",
|
|
||||||
replacement: {
|
|
||||||
match: /(?=this\.setState\()/,
|
|
||||||
replace: "$self.handleCrash(this)||"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: 'dispatch({type:"MODAL_POP_ALL"})',
|
|
||||||
replacement: {
|
|
||||||
match: /"MODAL_POP_ALL".+?};(?<=(\i)=function.+?)/,
|
|
||||||
replace: (m, popAll) => `${m}$self.popAllModals=${popAll};`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
handleCrash(_this: ReactElement & { forceUpdate: () => void; }) {
|
|
||||||
if (Date.now() - lastCrashTimestamp <= 1_000 && !shouldAttemptNextHandle) return true;
|
|
||||||
|
|
||||||
shouldAttemptNextHandle = false;
|
|
||||||
|
|
||||||
if (++crashCount > 5) {
|
|
||||||
try {
|
|
||||||
showNotification({
|
|
||||||
color: "#eed202",
|
|
||||||
title: "Discord has crashed!",
|
|
||||||
body: "Awn :( Discord has crashed more than five times, not attempting to recover.",
|
|
||||||
noPersist: true,
|
|
||||||
});
|
|
||||||
} catch { }
|
|
||||||
|
|
||||||
lastCrashTimestamp = Date.now();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => crashCount--, 60_000);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (crashCount === 1) maybePromptToUpdate("Uh oh, Discord has just crashed... but good news, there is a Vencord update available that might fix this issue! Would you like to update now?", true);
|
|
||||||
|
|
||||||
if (settings.store.attemptToPreventCrashes) {
|
|
||||||
this.handlePreventCrash(_this);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
} catch (err) {
|
|
||||||
CrashHandlerLogger.error("Failed to handle crash", err);
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
lastCrashTimestamp = Date.now();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
handlePreventCrash(_this: ReactElement & { forceUpdate: () => void; }) {
|
|
||||||
if (Date.now() - lastCrashTimestamp >= 1_000) {
|
|
||||||
try {
|
|
||||||
showNotification({
|
|
||||||
color: "#eed202",
|
|
||||||
title: "Discord has crashed!",
|
|
||||||
body: "Attempting to recover...",
|
|
||||||
noPersist: true,
|
|
||||||
});
|
|
||||||
} catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" });
|
|
||||||
} catch (err) {
|
|
||||||
CrashHandlerLogger.debug("Failed to close open context menu.", err);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
this.popAllModals?.();
|
|
||||||
} catch (err) {
|
|
||||||
CrashHandlerLogger.debug("Failed to close old modals.", err);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
closeAllModals();
|
|
||||||
} catch (err) {
|
|
||||||
CrashHandlerLogger.debug("Failed to close all open modals.", err);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
FluxDispatcher.dispatch({ type: "USER_PROFILE_MODAL_CLOSE" });
|
|
||||||
} catch (err) {
|
|
||||||
CrashHandlerLogger.debug("Failed to close user popout.", err);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
FluxDispatcher.dispatch({ type: "LAYER_POP_ALL" });
|
|
||||||
} catch (err) {
|
|
||||||
CrashHandlerLogger.debug("Failed to pop all layers.", err);
|
|
||||||
}
|
|
||||||
if (settings.store.attemptToNavigateToHome) {
|
|
||||||
try {
|
|
||||||
NavigationRouter.transitionTo("/channels/@me");
|
|
||||||
} catch (err) {
|
|
||||||
CrashHandlerLogger.debug("Failed to navigate to home", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
shouldAttemptNextHandle = true;
|
|
||||||
_this.forceUpdate();
|
|
||||||
} catch (err) {
|
|
||||||
CrashHandlerLogger.debug("Failed to update crash handler component.", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
@ -19,7 +19,6 @@
|
|||||||
import { definePluginSettings } from "@api/settings";
|
import { definePluginSettings } from "@api/settings";
|
||||||
import { Link } from "@components/Link";
|
import { Link } from "@components/Link";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { isTruthy } from "@utils/guards";
|
|
||||||
import { useAwaiter } from "@utils/misc";
|
import { useAwaiter } from "@utils/misc";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { filters, findByCodeLazy, findByPropsLazy, mapMangledModuleLazy } from "@webpack";
|
import { filters, findByCodeLazy, findByPropsLazy, mapMangledModuleLazy } from "@webpack";
|
||||||
@ -57,11 +56,11 @@ interface ActivityAssets {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Activity {
|
interface Activity {
|
||||||
state?: string;
|
state: string;
|
||||||
details?: string;
|
details?: string;
|
||||||
timestamps?: {
|
timestamps?: {
|
||||||
start?: number;
|
start?: Number;
|
||||||
end?: number;
|
end?: Number;
|
||||||
};
|
};
|
||||||
assets?: ActivityAssets;
|
assets?: ActivityAssets;
|
||||||
buttons?: Array<string>;
|
buttons?: Array<string>;
|
||||||
@ -71,7 +70,7 @@ interface Activity {
|
|||||||
button_urls?: Array<string>;
|
button_urls?: Array<string>;
|
||||||
};
|
};
|
||||||
type: ActivityType;
|
type: ActivityType;
|
||||||
flags: number;
|
flags: Number;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ActivityType {
|
enum ActivityType {
|
||||||
@ -94,13 +93,13 @@ const numOpt = (description: string) => ({
|
|||||||
onChange: setRpc
|
onChange: setRpc
|
||||||
}) as const;
|
}) as const;
|
||||||
|
|
||||||
const choice = (label: string, value: any, _default?: boolean) => ({
|
const choice = (label: string, value: any, _default?: Boolean) => ({
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
default: _default
|
default: _default
|
||||||
}) as const;
|
}) as const;
|
||||||
|
|
||||||
const choiceOpt = <T,>(description: string, options: T) => ({
|
const choiceOpt = (description: string, options) => ({
|
||||||
type: OptionType.SELECT,
|
type: OptionType.SELECT,
|
||||||
description,
|
description,
|
||||||
onChange: setRpc,
|
onChange: setRpc,
|
||||||
@ -174,13 +173,13 @@ async function createActivity(): Promise<Activity | undefined> {
|
|||||||
activity.buttons = [
|
activity.buttons = [
|
||||||
buttonOneText,
|
buttonOneText,
|
||||||
buttonTwoText
|
buttonTwoText
|
||||||
].filter(isTruthy);
|
].filter(Boolean);
|
||||||
|
|
||||||
activity.metadata = {
|
activity.metadata = {
|
||||||
button_urls: [
|
button_urls: [
|
||||||
buttonOneURL,
|
buttonOneURL,
|
||||||
buttonTwoURL
|
buttonTwoURL
|
||||||
].filter(isTruthy)
|
].filter(Boolean)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -207,16 +206,17 @@ async function createActivity(): Promise<Activity | undefined> {
|
|||||||
delete activity[k];
|
delete activity[k];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WHAT DO YOU WANT FROM ME
|
||||||
|
// eslint-disable-next-line consistent-return
|
||||||
return activity;
|
return activity;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setRpc(disable?: boolean) {
|
async function setRpc(disable?: Boolean) {
|
||||||
const activity: Activity | undefined = await createActivity();
|
const activity: Activity | undefined = await createActivity();
|
||||||
|
|
||||||
FluxDispatcher.dispatch({
|
FluxDispatcher.dispatch({
|
||||||
type: "LOCAL_ACTIVITY_UPDATE",
|
type: "LOCAL_ACTIVITY_UPDATE",
|
||||||
activity: !disable ? activity : null,
|
activity: !disable ? activity : {}
|
||||||
socketId: "CustomRPC",
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,270 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2023 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
|
|
||||||
import { showNotification } from "@api/Notifications";
|
|
||||||
import { definePluginSettings } from "@api/settings";
|
|
||||||
import { Devs } from "@utils/constants";
|
|
||||||
import Logger from "@utils/Logger";
|
|
||||||
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
|
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
|
||||||
import { filters, findAll, search } from "@webpack";
|
|
||||||
import { Menu } from "@webpack/common";
|
|
||||||
|
|
||||||
const PORT = 8485;
|
|
||||||
const NAV_ID = "dev-companion-reconnect";
|
|
||||||
|
|
||||||
const logger = new Logger("DevCompanion");
|
|
||||||
|
|
||||||
let socket: WebSocket | undefined;
|
|
||||||
|
|
||||||
type Node = StringNode | RegexNode | FunctionNode;
|
|
||||||
|
|
||||||
interface StringNode {
|
|
||||||
type: "string";
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RegexNode {
|
|
||||||
type: "regex";
|
|
||||||
value: {
|
|
||||||
pattern: string;
|
|
||||||
flags: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FunctionNode {
|
|
||||||
type: "function";
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PatchData {
|
|
||||||
find: string;
|
|
||||||
replacement: {
|
|
||||||
match: StringNode | RegexNode;
|
|
||||||
replace: StringNode | FunctionNode;
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FindData {
|
|
||||||
type: string;
|
|
||||||
args: Array<StringNode | FunctionNode>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
|
||||||
notifyOnAutoConnect: {
|
|
||||||
description: "Whether to notify when Dev Companion has automatically connected.",
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
default: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function parseNode(node: Node) {
|
|
||||||
switch (node.type) {
|
|
||||||
case "string":
|
|
||||||
return node.value;
|
|
||||||
case "regex":
|
|
||||||
return new RegExp(node.value.pattern, node.value.flags);
|
|
||||||
case "function":
|
|
||||||
// We LOVE remote code execution
|
|
||||||
// Safety: This comes from localhost only, which actually means we have less permissions than the source,
|
|
||||||
// since we're running in the browser sandbox, whereas the sender has host access
|
|
||||||
return (0, eval)(node.value);
|
|
||||||
default:
|
|
||||||
throw new Error("Unknown Node Type " + (node as any).type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function initWs(isManual = false) {
|
|
||||||
let wasConnected = isManual;
|
|
||||||
let hasErrored = false;
|
|
||||||
const ws = socket = new WebSocket(`ws://localhost:${PORT}`);
|
|
||||||
|
|
||||||
ws.addEventListener("open", () => {
|
|
||||||
wasConnected = true;
|
|
||||||
|
|
||||||
logger.info("Connected to WebSocket");
|
|
||||||
|
|
||||||
(settings.store.notifyOnAutoConnect || isManual) && showNotification({
|
|
||||||
title: "Dev Companion Connected",
|
|
||||||
body: "Connected to WebSocket",
|
|
||||||
noPersist: true
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
ws.addEventListener("error", e => {
|
|
||||||
if (!wasConnected) return;
|
|
||||||
|
|
||||||
hasErrored = true;
|
|
||||||
|
|
||||||
logger.error("Dev Companion Error:", e);
|
|
||||||
|
|
||||||
showNotification({
|
|
||||||
title: "Dev Companion Error",
|
|
||||||
body: (e as ErrorEvent).message || "No Error Message",
|
|
||||||
color: "var(--status-danger, red)",
|
|
||||||
noPersist: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
ws.addEventListener("close", e => {
|
|
||||||
if (!wasConnected || hasErrored) return;
|
|
||||||
|
|
||||||
logger.info("Dev Companion Disconnected:", e.code, e.reason);
|
|
||||||
|
|
||||||
showNotification({
|
|
||||||
title: "Dev Companion Disconnected",
|
|
||||||
body: e.reason || "No Reason provided",
|
|
||||||
color: "var(--status-danger, red)",
|
|
||||||
noPersist: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
ws.addEventListener("message", e => {
|
|
||||||
try {
|
|
||||||
var { nonce, type, data } = JSON.parse(e.data);
|
|
||||||
} catch (err) {
|
|
||||||
logger.error("Invalid JSON:", err, "\n" + e.data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
function reply(error?: string) {
|
|
||||||
const data = { nonce, ok: !error } as Record<string, unknown>;
|
|
||||||
if (error) data.error = error;
|
|
||||||
|
|
||||||
ws.send(JSON.stringify(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info("Received Message:", type, "\n", data);
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case "testPatch": {
|
|
||||||
const { find, replacement } = data as PatchData;
|
|
||||||
|
|
||||||
const candidates = search(find);
|
|
||||||
const keys = Object.keys(candidates);
|
|
||||||
if (keys.length !== 1)
|
|
||||||
return reply("Expected exactly one 'find' matches, found " + keys.length);
|
|
||||||
|
|
||||||
const mod = candidates[keys[0]];
|
|
||||||
let src = String(mod.original ?? mod).replaceAll("\n", "");
|
|
||||||
|
|
||||||
if (src.startsWith("function(")) {
|
|
||||||
src = "0," + src;
|
|
||||||
}
|
|
||||||
|
|
||||||
let i = 0;
|
|
||||||
|
|
||||||
for (const { match, replace } of replacement) {
|
|
||||||
i++;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const matcher = canonicalizeMatch(parseNode(match));
|
|
||||||
const replacement = canonicalizeReplace(parseNode(replace), "PlaceHolderPluginName");
|
|
||||||
|
|
||||||
const newSource = src.replace(matcher, replacement as string);
|
|
||||||
|
|
||||||
if (src === newSource) throw "Had no effect";
|
|
||||||
Function(newSource);
|
|
||||||
|
|
||||||
src = newSource;
|
|
||||||
} catch (err) {
|
|
||||||
return reply(`Replacement ${i} failed: ${err}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
reply();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "testFind": {
|
|
||||||
const { type, args } = data as FindData;
|
|
||||||
try {
|
|
||||||
var parsedArgs = args.map(parseNode);
|
|
||||||
} catch (err) {
|
|
||||||
return reply("Failed to parse args: " + err);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
let results: any[];
|
|
||||||
switch (type.replace("find", "").replace("Lazy", "")) {
|
|
||||||
case "":
|
|
||||||
results = findAll(parsedArgs[0]);
|
|
||||||
break;
|
|
||||||
case "ByProps":
|
|
||||||
results = findAll(filters.byProps(...parsedArgs));
|
|
||||||
break;
|
|
||||||
case "Store":
|
|
||||||
results = findAll(filters.byStoreName(parsedArgs[0]));
|
|
||||||
break;
|
|
||||||
case "ByCode":
|
|
||||||
results = findAll(filters.byCode(...parsedArgs));
|
|
||||||
break;
|
|
||||||
case "ModuleId":
|
|
||||||
results = Object.keys(search(parsedArgs[0]));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return reply("Unknown Find Type " + type);
|
|
||||||
}
|
|
||||||
|
|
||||||
const uniqueResultsCount = new Set(results).size;
|
|
||||||
if (uniqueResultsCount === 0) throw "No results";
|
|
||||||
if (uniqueResultsCount > 1) throw "Found more than one result! Make this filter more specific";
|
|
||||||
} catch (err) {
|
|
||||||
return reply("Failed to find: " + err);
|
|
||||||
}
|
|
||||||
|
|
||||||
reply();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
reply("Unknown Type " + type);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const contextMenuPatch: NavContextMenuPatchCallback = children => () => {
|
|
||||||
children.unshift(
|
|
||||||
<Menu.MenuItem
|
|
||||||
id={NAV_ID}
|
|
||||||
label="Reconnect Dev Companion"
|
|
||||||
action={() => {
|
|
||||||
socket?.close(1000, "Reconnecting");
|
|
||||||
initWs(true);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "DevCompanion",
|
|
||||||
description: "Dev Companion Plugin",
|
|
||||||
authors: [Devs.Ven],
|
|
||||||
settings,
|
|
||||||
|
|
||||||
start() {
|
|
||||||
initWs();
|
|
||||||
addContextMenuPatch("user-settings-cog", contextMenuPatch);
|
|
||||||
},
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
socket?.close(1000, "Plugin Stopped");
|
|
||||||
socket = void 0;
|
|
||||||
removeContextMenuPatch("user-settings-cog", contextMenuPatch);
|
|
||||||
}
|
|
||||||
});
|
|
@ -23,7 +23,7 @@ import definePlugin from "@utils/types";
|
|||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "UrbanDictionary",
|
name: "UrbanDictionary",
|
||||||
description: "Search for a word on Urban Dictionary via /urban slash command",
|
description: "Searches for a word on Urban Dictionary",
|
||||||
authors: [Devs.jewdev],
|
authors: [Devs.jewdev],
|
||||||
dependencies: ["CommandsAPI"],
|
dependencies: ["CommandsAPI"],
|
||||||
commands: [
|
commands: [
|
@ -27,9 +27,9 @@ export default definePlugin({
|
|||||||
{
|
{
|
||||||
find: ".Messages.BOT_CALL_IDLE_DISCONNECT",
|
find: ".Messages.BOT_CALL_IDLE_DISCONNECT",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<=function \i\(\){)(?=.{1,100}\.Messages\.BOT_CALL_IDLE_DISCONNECT)/,
|
match: /function (?<functionName>.{1,3})\(\){.{1,100}\.Messages\.BOT_CALL_IDLE_DISCONNECT.+?}}/,
|
||||||
replace: "return;"
|
replace: "function $<functionName>(){}",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
|
@ -16,15 +16,15 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
|
import { migratePluginSettings, Settings } from "@api/settings";
|
||||||
import { CheckedTextInput } from "@components/CheckedTextInput";
|
import { CheckedTextInput } from "@components/CheckedTextInput";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import Logger from "@utils/Logger";
|
import Logger from "@utils/Logger";
|
||||||
import { Margins } from "@utils/margins";
|
import { makeLazy } from "@utils/misc";
|
||||||
import { ModalContent, ModalHeader, ModalRoot, openModal } from "@utils/modal";
|
import { ModalContent, ModalHeader, ModalRoot, openModal } from "@utils/modal";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { findByCodeLazy, findByPropsLazy } from "@webpack";
|
import { findByCodeLazy, findByPropsLazy } from "@webpack";
|
||||||
import { Forms, GuildStore, Menu, PermissionStore, React, Toasts, Tooltip, UserStore } from "@webpack/common";
|
import { Forms, GuildStore, Margins, Menu, PermissionStore, React, Toasts, Tooltip, UserStore } from "@webpack/common";
|
||||||
|
|
||||||
const MANAGE_EMOJIS_AND_STICKERS = 1n << 30n;
|
const MANAGE_EMOJIS_AND_STICKERS = 1n << 30n;
|
||||||
|
|
||||||
@ -96,7 +96,7 @@ function CloneModal({ id, name: emojiName, isAnimated }: { id: string; name: str
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Forms.FormTitle className={Margins.top20}>Custom Name</Forms.FormTitle>
|
<Forms.FormTitle className={Margins.marginTop20}>Custom Name</Forms.FormTitle>
|
||||||
<CheckedTextInput
|
<CheckedTextInput
|
||||||
value={name}
|
value={name}
|
||||||
onChange={setName}
|
onChange={setName}
|
||||||
@ -175,12 +175,50 @@ function CloneModal({ id, name: emojiName, isAnimated }: { id: string; name: str
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildMenuItem(id: string, name: string, isAnimated: boolean) {
|
migratePluginSettings("EmoteCloner", "EmoteYoink");
|
||||||
return (
|
export default definePlugin({
|
||||||
<Menu.MenuItem
|
name: "EmoteCloner",
|
||||||
|
description: "Adds a Clone context menu item to emotes to clone them your own server",
|
||||||
|
authors: [Devs.Ven],
|
||||||
|
dependencies: ["MenuItemDeobfuscatorAPI"],
|
||||||
|
|
||||||
|
patches: [{
|
||||||
|
// Literally copy pasted from ReverseImageSearch lol
|
||||||
|
find: "open-native-link",
|
||||||
|
replacement: {
|
||||||
|
match: /id:"open-native-link".{0,200}\(\{href:(.{0,3}),.{0,200}\},"open-native-link"\)/,
|
||||||
|
replace: "$&,$self.makeMenu(arguments[2])"
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
// Also copy pasted from Reverse Image Search
|
||||||
|
{
|
||||||
|
// pass the target to the open link menu so we can grab its data
|
||||||
|
find: "REMOVE_ALL_REACTIONS_CONFIRM_BODY,",
|
||||||
|
predicate: makeLazy(() => !Settings.plugins.ReverseImageSearch.enabled),
|
||||||
|
noWarn: true,
|
||||||
|
replacement: {
|
||||||
|
match: /(?<props>.).onHeightUpdate.{0,200}(.)=(.)=.\.url;.+?\(null!=\3\?\3:\2[^)]+/,
|
||||||
|
replace: "$&,$<props>.target"
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
|
||||||
|
makeMenu(htmlElement: HTMLImageElement) {
|
||||||
|
if (htmlElement?.dataset.type !== "emoji")
|
||||||
|
return null;
|
||||||
|
|
||||||
|
const { id } = htmlElement.dataset;
|
||||||
|
const name = htmlElement.alt.match(/:(.*)(?:~\d+)?:/)?.[1];
|
||||||
|
|
||||||
|
if (!name || !id)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
const isAnimated = new URL(htmlElement.src).pathname.endsWith(".gif");
|
||||||
|
|
||||||
|
return <Menu.MenuItem
|
||||||
id="emote-cloner"
|
id="emote-cloner"
|
||||||
key="emote-cloner"
|
key="emote-cloner"
|
||||||
label="Clone Emote"
|
label="Clone"
|
||||||
action={() =>
|
action={() =>
|
||||||
openModal(modalProps => (
|
openModal(modalProps => (
|
||||||
<ModalRoot {...modalProps}>
|
<ModalRoot {...modalProps}>
|
||||||
@ -202,48 +240,7 @@ function buildMenuItem(id: string, name: string, isAnimated: boolean) {
|
|||||||
</ModalRoot>
|
</ModalRoot>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
/>
|
>
|
||||||
);
|
</Menu.MenuItem>;
|
||||||
}
|
|
||||||
|
|
||||||
function isGifUrl(url: string) {
|
|
||||||
return new URL(url).pathname.endsWith(".gif");
|
|
||||||
}
|
|
||||||
|
|
||||||
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => {
|
|
||||||
const { favoriteableId, itemHref, itemSrc, favoriteableType } = props ?? {};
|
|
||||||
|
|
||||||
if (!favoriteableId || favoriteableType !== "emoji") return;
|
|
||||||
|
|
||||||
const match = props.message.content.match(RegExp(`<a?:(\\w+)(?:~\\d+)?:${favoriteableId}>|https://cdn\\.discordapp\\.com/emojis/${favoriteableId}\\.`));
|
|
||||||
if (!match) return;
|
|
||||||
const name = match[1] ?? "FakeNitroEmoji";
|
|
||||||
|
|
||||||
const group = findGroupChildrenByChildId("copy-link", children);
|
|
||||||
if (group) group.push(buildMenuItem(favoriteableId, name, isGifUrl(itemHref ?? itemSrc)));
|
|
||||||
};
|
|
||||||
|
|
||||||
const expressionPickerPatch: NavContextMenuPatchCallback = (children, props: { target: HTMLElement; }) => () => {
|
|
||||||
const { id, name, type } = props?.target?.dataset ?? {};
|
|
||||||
if (!id || !name || type !== "emoji") return;
|
|
||||||
|
|
||||||
const firstChild = props.target.firstChild as HTMLImageElement;
|
|
||||||
|
|
||||||
children.push(buildMenuItem(id, name, firstChild && isGifUrl(firstChild.src)));
|
|
||||||
};
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "EmoteCloner",
|
|
||||||
description: "Adds a Clone context menu item to emotes to clone them your own server",
|
|
||||||
authors: [Devs.Ven, Devs.Nuckyz],
|
|
||||||
|
|
||||||
start() {
|
|
||||||
addContextMenuPatch("message", messageContextMenuPatch);
|
|
||||||
addContextMenuPatch("expression-picker", expressionPickerPatch);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
stop() {
|
|
||||||
removeContextMenuPatch("message", messageContextMenuPatch);
|
|
||||||
removeContextMenuPatch("expression-picker", expressionPickerPatch);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { definePluginSettings } from "@api/settings";
|
import { Settings } from "@api/settings";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
@ -24,71 +24,49 @@ import { Forms, React } from "@webpack/common";
|
|||||||
|
|
||||||
const KbdStyles = findByPropsLazy("key", "removeBuildOverride");
|
const KbdStyles = findByPropsLazy("key", "removeBuildOverride");
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
|
||||||
enableIsStaff: {
|
|
||||||
description: "Enable isStaff",
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
default: false,
|
|
||||||
restartNeeded: true
|
|
||||||
},
|
|
||||||
forceStagingBanner: {
|
|
||||||
description: "Whether to force Staging banner under user area.",
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
default: false,
|
|
||||||
restartNeeded: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "Experiments",
|
name: "Experiments",
|
||||||
description: "Enable Access to Experiments in Discord!",
|
|
||||||
authors: [
|
authors: [
|
||||||
Devs.Megu,
|
Devs.Megu,
|
||||||
Devs.Ven,
|
Devs.Ven,
|
||||||
Devs.Nickyux,
|
Devs.Nickyux,
|
||||||
Devs.BanTheNons,
|
Devs.BanTheNons
|
||||||
Devs.Nuckyz
|
|
||||||
],
|
],
|
||||||
settings,
|
description: "Enable Access to Experiments in Discord!",
|
||||||
|
patches: [{
|
||||||
patches: [
|
|
||||||
{
|
|
||||||
find: "Object.defineProperties(this,{isDeveloper",
|
find: "Object.defineProperties(this,{isDeveloper",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<={isDeveloper:\{[^}]+?,get:function\(\)\{return )\w/,
|
match: /(?<={isDeveloper:\{[^}]+,get:function\(\)\{return )\w/,
|
||||||
replace: "true"
|
replace: "true"
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
}, {
|
||||||
find: 'type:"user",revision',
|
find: 'type:"user",revision',
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /!(\i)&&"CONNECTION_OPEN".+?;/g,
|
match: /!(\w{1,3})&&"CONNECTION_OPEN".+?;/g,
|
||||||
replace: "$1=!0;"
|
replace: "$1=!0;"
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
}, {
|
||||||
find: ".isStaff=function(){",
|
find: ".isStaff=function(){",
|
||||||
predicate: () => settings.store.enableIsStaff,
|
predicate: () => Settings.plugins.Experiments.enableIsStaff === true,
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /return\s*?(\i)\.hasFlag\((\i\.\i)\.STAFF\)}/,
|
match: /return\s*(\w+)\.hasFlag\((.+?)\.STAFF\)}/,
|
||||||
replace: (_, user, flags) => `return Vencord.Webpack.Common.UserStore.getCurrentUser().id===${user}.id||${user}.hasFlag(${flags}.STAFF)}`
|
replace: "return Vencord.Webpack.Common.UserStore.getCurrentUser().id===$1.id||$1.hasFlag($2.STAFF)}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /hasFreePremium=function\(\){return this.isStaff\(\)\s*?\|\|/,
|
match: /hasFreePremium=function\(\){return this.isStaff\(\)\s*\|\|/,
|
||||||
replace: "hasFreePremium=function(){return ",
|
replace: "hasFreePremium=function(){return ",
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
|
||||||
find: ".Messages.DEV_NOTICE_STAGING",
|
|
||||||
predicate: () => settings.store.forceStagingBanner,
|
|
||||||
replacement: {
|
|
||||||
match: /"staging"===window\.GLOBAL_ENV\.RELEASE_CHANNEL/,
|
|
||||||
replace: "true"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
|
}],
|
||||||
|
options: {
|
||||||
|
enableIsStaff: {
|
||||||
|
description: "Enable isStaff (requires restart)",
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
default: false,
|
||||||
|
restartNeeded: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
settingsAboutComponent: () => {
|
settingsAboutComponent: () => {
|
||||||
const isMacOS = navigator.platform.includes("Mac");
|
const isMacOS = navigator.platform.includes("Mac");
|
||||||
|
@ -1,42 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2023 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Devs } from "@utils/constants";
|
|
||||||
import definePlugin from "@utils/types";
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "F8Break",
|
|
||||||
description: "Pause the client when you press F8 with DevTools (+ breakpoints) open.",
|
|
||||||
authors: [Devs.lewisakura],
|
|
||||||
|
|
||||||
start() {
|
|
||||||
window.addEventListener("keydown", this.event);
|
|
||||||
},
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
window.removeEventListener("keydown", this.event);
|
|
||||||
},
|
|
||||||
|
|
||||||
event(e: KeyboardEvent) {
|
|
||||||
if (e.code === "F8") {
|
|
||||||
// Hi! You've just paused the client. Pressing F8 in DevTools or in the main window will unpause it again.
|
|
||||||
// It's up to you on what to do, friend. Happy travels!
|
|
||||||
debugger;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
359
src/plugins/fakeNitro.ts
Normal file
359
src/plugins/fakeNitro.ts
Normal file
@ -0,0 +1,359 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { addPreEditListener, addPreSendListener, removePreEditListener, removePreSendListener } from "@api/MessageEvents";
|
||||||
|
import { migratePluginSettings, Settings } from "@api/settings";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import { ApngDisposeOp, getGifEncoder, importApngJs } from "@utils/dependencies";
|
||||||
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
|
import { findByCodeLazy, findByPropsLazy } from "@webpack";
|
||||||
|
import { ChannelStore, PermissionStore, UserStore } from "@webpack/common";
|
||||||
|
|
||||||
|
const DRAFT_TYPE = 0;
|
||||||
|
const promptToUpload = findByCodeLazy("UPLOAD_FILE_LIMIT_ERROR");
|
||||||
|
|
||||||
|
const USE_EXTERNAL_EMOJIS = 1n << 18n;
|
||||||
|
const USE_EXTERNAL_STICKERS = 1n << 37n;
|
||||||
|
|
||||||
|
enum EmojiIntentions {
|
||||||
|
REACTION = 0,
|
||||||
|
STATUS = 1,
|
||||||
|
COMMUNITY_CONTENT = 2,
|
||||||
|
CHAT = 3,
|
||||||
|
GUILD_STICKER_RELATED_EMOJI = 4,
|
||||||
|
GUILD_ROLE_BENEFIT_EMOJI = 5,
|
||||||
|
COMMUNITY_CONTENT_ONLY = 6,
|
||||||
|
SOUNDBOARD = 7
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BaseSticker {
|
||||||
|
available: boolean;
|
||||||
|
description: string;
|
||||||
|
format_type: number;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
tags: string;
|
||||||
|
type: number;
|
||||||
|
}
|
||||||
|
interface GuildSticker extends BaseSticker {
|
||||||
|
guild_id: string;
|
||||||
|
}
|
||||||
|
interface DiscordSticker extends BaseSticker {
|
||||||
|
pack_id: string;
|
||||||
|
}
|
||||||
|
type Sticker = GuildSticker | DiscordSticker;
|
||||||
|
|
||||||
|
interface StickerPack {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
sku_id: string;
|
||||||
|
description: string;
|
||||||
|
cover_sticker_id: string;
|
||||||
|
banner_asset_id: string;
|
||||||
|
stickers: Sticker[];
|
||||||
|
}
|
||||||
|
|
||||||
|
migratePluginSettings("FakeNitro", "NitroBypass");
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "FakeNitro",
|
||||||
|
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.obscurity, Devs.captain],
|
||||||
|
description: "Allows you to stream in nitro quality, send fake emojis/stickers and use client themes.",
|
||||||
|
dependencies: ["MessageEventsAPI"],
|
||||||
|
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: ".PREMIUM_LOCKED;",
|
||||||
|
predicate: () => Settings.plugins.FakeNitro.enableEmojiBypass === true,
|
||||||
|
replacement: [
|
||||||
|
{
|
||||||
|
match: /(?<=(?<intention>\i)=\i\.intention)/,
|
||||||
|
replace: ",fakeNitroIntention=$<intention>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: /(?<=\.(?:canUseEmojisEverywhere|canUseAnimatedEmojis)\(\i)(?=\))/g,
|
||||||
|
replace: ",fakeNitroIntention"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: /(?<=&&!\i&&)!(?<canUseExternal>\i)(?=\)return \i\.\i\.DISALLOW_EXTERNAL;)/,
|
||||||
|
replace: `(!$<canUseExternal>&&![${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention))`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: "canUseAnimatedEmojis:function",
|
||||||
|
predicate: () => Settings.plugins.FakeNitro.enableEmojiBypass === true,
|
||||||
|
replacement: {
|
||||||
|
match: /(?<=(?:canUseEmojisEverywhere|canUseAnimatedEmojis):function\((?<user>\i))\){(?<premiumCheck>.+?\))/g,
|
||||||
|
replace: `,fakeNitroIntention){$<premiumCheck>||fakeNitroIntention===undefined||[${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: "canUseStickersEverywhere:function",
|
||||||
|
predicate: () => Settings.plugins.FakeNitro.enableStickerBypass === true,
|
||||||
|
replacement: {
|
||||||
|
match: /canUseStickersEverywhere:function\(.+?\{/,
|
||||||
|
replace: "$&return true;"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: "\"SENDABLE\"",
|
||||||
|
predicate: () => Settings.plugins.FakeNitro.enableStickerBypass === true,
|
||||||
|
replacement: {
|
||||||
|
match: /(\w+)\.available\?/,
|
||||||
|
replace: "true?"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: "canStreamHighQuality:function",
|
||||||
|
predicate: () => Settings.plugins.FakeNitro.enableStreamQualityBypass === true,
|
||||||
|
replacement: [
|
||||||
|
"canUseHighVideoUploadQuality",
|
||||||
|
"canStreamHighQuality",
|
||||||
|
"canStreamMidQuality"
|
||||||
|
].map(func => {
|
||||||
|
return {
|
||||||
|
match: new RegExp(`${func}:function\\(.+?\\{`),
|
||||||
|
replace: "$&return true;"
|
||||||
|
};
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: "STREAM_FPS_OPTION.format",
|
||||||
|
predicate: () => Settings.plugins.FakeNitro.enableStreamQualityBypass === true,
|
||||||
|
replacement: {
|
||||||
|
match: /(userPremiumType|guildPremiumTier):.{0,10}TIER_\d,?/g,
|
||||||
|
replace: ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: "canUseClientThemes:function",
|
||||||
|
replacement: {
|
||||||
|
match: /(?<=canUseClientThemes:function\(\i\){)/,
|
||||||
|
replace: "return true;"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
options: {
|
||||||
|
enableEmojiBypass: {
|
||||||
|
description: "Allow sending fake emojis",
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
default: true,
|
||||||
|
restartNeeded: true,
|
||||||
|
},
|
||||||
|
emojiSize: {
|
||||||
|
description: "Size of the emojis when sending",
|
||||||
|
type: OptionType.SLIDER,
|
||||||
|
default: 48,
|
||||||
|
markers: [32, 48, 64, 128, 160, 256, 512],
|
||||||
|
},
|
||||||
|
enableStickerBypass: {
|
||||||
|
description: "Allow sending fake stickers",
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
default: true,
|
||||||
|
restartNeeded: true,
|
||||||
|
},
|
||||||
|
stickerSize: {
|
||||||
|
description: "Size of the stickers when sending",
|
||||||
|
type: OptionType.SLIDER,
|
||||||
|
default: 160,
|
||||||
|
markers: [32, 64, 128, 160, 256, 512],
|
||||||
|
},
|
||||||
|
enableStreamQualityBypass: {
|
||||||
|
description: "Allow streaming in nitro quality",
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
default: true,
|
||||||
|
restartNeeded: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
get guildId() {
|
||||||
|
return window.location.href.split("channels/")[1].split("/")[0];
|
||||||
|
},
|
||||||
|
|
||||||
|
get canUseEmotes() {
|
||||||
|
return (UserStore.getCurrentUser().premiumType ?? 0) > 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
get canUseStickers() {
|
||||||
|
return (UserStore.getCurrentUser().premiumType ?? 0) > 1;
|
||||||
|
},
|
||||||
|
|
||||||
|
hasPermissionToUseExternalEmojis(channelId: string) {
|
||||||
|
const channel = ChannelStore.getChannel(channelId);
|
||||||
|
|
||||||
|
if (!channel || channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM()) return true;
|
||||||
|
|
||||||
|
return PermissionStore.can(USE_EXTERNAL_EMOJIS, channel);
|
||||||
|
},
|
||||||
|
|
||||||
|
hasPermissionToUseExternalStickers(channelId: string) {
|
||||||
|
const channel = ChannelStore.getChannel(channelId);
|
||||||
|
|
||||||
|
if (!channel || channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM()) return true;
|
||||||
|
|
||||||
|
return PermissionStore.can(USE_EXTERNAL_STICKERS, channel);
|
||||||
|
},
|
||||||
|
|
||||||
|
getStickerLink(stickerId: string) {
|
||||||
|
return `https://media.discordapp.net/stickers/${stickerId}.png?size=${Settings.plugins.FakeNitro.stickerSize}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
async sendAnimatedSticker(stickerLink: string, stickerId: string, channelId: string) {
|
||||||
|
const [{ parseURL }, {
|
||||||
|
GIFEncoder,
|
||||||
|
quantize,
|
||||||
|
applyPalette
|
||||||
|
}] = await Promise.all([importApngJs(), getGifEncoder()]);
|
||||||
|
|
||||||
|
const { frames, width, height } = await parseURL(stickerLink);
|
||||||
|
|
||||||
|
const gif = new GIFEncoder();
|
||||||
|
const resolution = Settings.plugins.FakeNitro.stickerSize;
|
||||||
|
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.width = resolution;
|
||||||
|
canvas.height = resolution;
|
||||||
|
|
||||||
|
const ctx = canvas.getContext("2d", {
|
||||||
|
willReadFrequently: true
|
||||||
|
})!;
|
||||||
|
|
||||||
|
const scale = resolution / Math.max(width, height);
|
||||||
|
ctx.scale(scale, scale);
|
||||||
|
|
||||||
|
let lastImg: HTMLImageElement | null = null;
|
||||||
|
for (const { left, top, width, height, disposeOp, img, delay } of frames) {
|
||||||
|
ctx.drawImage(img, left, top, width, height);
|
||||||
|
|
||||||
|
const { data } = ctx.getImageData(0, 0, resolution, resolution);
|
||||||
|
|
||||||
|
const palette = quantize(data, 256);
|
||||||
|
const index = applyPalette(data, palette);
|
||||||
|
|
||||||
|
gif.writeFrame(index, resolution, resolution, {
|
||||||
|
transparent: true,
|
||||||
|
palette,
|
||||||
|
delay,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (disposeOp === ApngDisposeOp.BACKGROUND) {
|
||||||
|
ctx.clearRect(left, top, width, height);
|
||||||
|
} else if (disposeOp === ApngDisposeOp.PREVIOUS && lastImg) {
|
||||||
|
ctx.drawImage(lastImg, left, top, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastImg = img;
|
||||||
|
}
|
||||||
|
|
||||||
|
gif.finish();
|
||||||
|
const file = new File([gif.bytesView()], `${stickerId}.gif`, { type: "image/gif" });
|
||||||
|
promptToUpload([file], ChannelStore.getChannel(channelId), DRAFT_TYPE);
|
||||||
|
},
|
||||||
|
|
||||||
|
start() {
|
||||||
|
const settings = Settings.plugins.FakeNitro;
|
||||||
|
if (!settings.enableEmojiBypass && !settings.enableStickerBypass) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EmojiStore = findByPropsLazy("getCustomEmojiById");
|
||||||
|
const StickerStore = findByPropsLazy("getAllGuildStickers") as {
|
||||||
|
getPremiumPacks(): StickerPack[];
|
||||||
|
getAllGuildStickers(): Map<string, Sticker[]>;
|
||||||
|
getStickerById(id: string): Sticker | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getWordBoundary(origStr: string, offset: number) {
|
||||||
|
return (!origStr[offset] || /\s/.test(origStr[offset])) ? "" : " ";
|
||||||
|
}
|
||||||
|
|
||||||
|
this.preSend = addPreSendListener((channelId, messageObj, extra) => {
|
||||||
|
const { guildId } = this;
|
||||||
|
|
||||||
|
stickerBypass: {
|
||||||
|
if (!settings.enableStickerBypass)
|
||||||
|
break stickerBypass;
|
||||||
|
|
||||||
|
const sticker = StickerStore.getStickerById(extra?.stickerIds?.[0]!);
|
||||||
|
if (!sticker)
|
||||||
|
break stickerBypass;
|
||||||
|
|
||||||
|
if (sticker.available !== false && ((this.canUseStickers && this.hasPermissionToUseExternalStickers(channelId)) || (sticker as GuildSticker)?.guild_id === guildId))
|
||||||
|
break stickerBypass;
|
||||||
|
|
||||||
|
let link = this.getStickerLink(sticker.id);
|
||||||
|
if (sticker.format_type === 2) {
|
||||||
|
this.sendAnimatedSticker(this.getStickerLink(sticker.id), sticker.id, channelId);
|
||||||
|
return { cancel: true };
|
||||||
|
} else {
|
||||||
|
if ("pack_id" in sticker) {
|
||||||
|
const packId = sticker.pack_id === "847199849233514549"
|
||||||
|
// Discord moved these stickers into a different pack at some point, but
|
||||||
|
// Distok still uses the old id
|
||||||
|
? "749043879713701898"
|
||||||
|
: sticker.pack_id;
|
||||||
|
|
||||||
|
link = `https://distok.top/stickers/${packId}/${sticker.id}.gif`;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete extra.stickerIds;
|
||||||
|
messageObj.content += " " + link;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((!this.canUseEmotes || !this.hasPermissionToUseExternalEmojis(channelId)) && settings.enableEmojiBypass) {
|
||||||
|
for (const emoji of messageObj.validNonShortcutEmojis) {
|
||||||
|
if (!emoji.require_colons) continue;
|
||||||
|
if (emoji.guildId === guildId && !emoji.animated) continue;
|
||||||
|
|
||||||
|
const emojiString = `<${emoji.animated ? "a" : ""}:${emoji.originalName || emoji.name}:${emoji.id}>`;
|
||||||
|
const url = emoji.url.replace(/\?size=\d+/, `?size=${Settings.plugins.FakeNitro.emojiSize}`);
|
||||||
|
messageObj.content = messageObj.content.replace(emojiString, (match, offset, origStr) => {
|
||||||
|
return `${getWordBoundary(origStr, offset - 1)}${url}${getWordBoundary(origStr, offset + match.length)}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { cancel: false };
|
||||||
|
});
|
||||||
|
|
||||||
|
this.preEdit = addPreEditListener((channelId, __, messageObj) => {
|
||||||
|
if (this.canUseEmotes && this.hasPermissionToUseExternalEmojis(channelId)) return;
|
||||||
|
|
||||||
|
const { guildId } = this;
|
||||||
|
|
||||||
|
for (const [emojiStr, _, emojiId] of messageObj.content.matchAll(/(?<!\\)<a?:(\w+):(\d+)>/ig)) {
|
||||||
|
const emoji = EmojiStore.getCustomEmojiById(emojiId);
|
||||||
|
if (emoji == null || (emoji.guildId === guildId && !emoji.animated)) continue;
|
||||||
|
if (!emoji.require_colons) continue;
|
||||||
|
|
||||||
|
const url = emoji.url.replace(/\?size=\d+/, `?size=${Settings.plugins.FakeNitro.emojiSize}`);
|
||||||
|
messageObj.content = messageObj.content.replace(emojiStr, (match, offset, origStr) => {
|
||||||
|
return `${getWordBoundary(origStr, offset - 1)}${url}${getWordBoundary(origStr, offset + match.length)}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
removePreSendListener(this.preSend);
|
||||||
|
removePreEditListener(this.preEdit);
|
||||||
|
}
|
||||||
|
});
|
@ -1,717 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { addPreEditListener, addPreSendListener, removePreEditListener, removePreSendListener } from "@api/MessageEvents";
|
|
||||||
import { definePluginSettings, migratePluginSettings, Settings } from "@api/settings";
|
|
||||||
import { Devs } from "@utils/constants";
|
|
||||||
import { ApngBlendOp, ApngDisposeOp, getGifEncoder, importApngJs } from "@utils/dependencies";
|
|
||||||
import { getCurrentGuild } from "@utils/discord";
|
|
||||||
import { proxyLazy } from "@utils/proxyLazy";
|
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
|
||||||
import { findByCodeLazy, findByPropsLazy, findLazy, findStoreLazy } from "@webpack";
|
|
||||||
import { ChannelStore, FluxDispatcher, Parser, PermissionStore, UserStore } from "@webpack/common";
|
|
||||||
import type { Message } from "discord-types/general";
|
|
||||||
import type { ReactNode } from "react";
|
|
||||||
|
|
||||||
const DRAFT_TYPE = 0;
|
|
||||||
const promptToUpload = findByCodeLazy("UPLOAD_FILE_LIMIT_ERROR");
|
|
||||||
const UserSettingsProtoStore = findStoreLazy("UserSettingsProtoStore");
|
|
||||||
const PreloadedUserSettingsProtoHandler = findLazy(m => m.ProtoClass?.typeName === "discord_protos.discord_users.v1.PreloadedUserSettings");
|
|
||||||
const ReaderFactory = findByPropsLazy("readerFactory");
|
|
||||||
const StickerStore = findStoreLazy("StickersStore") as {
|
|
||||||
getPremiumPacks(): StickerPack[];
|
|
||||||
getAllGuildStickers(): Map<string, Sticker[]>;
|
|
||||||
getStickerById(id: string): Sticker | undefined;
|
|
||||||
};
|
|
||||||
const EmojiStore = findStoreLazy("EmojiStore");
|
|
||||||
|
|
||||||
|
|
||||||
function searchProtoClass(localName: string, parentProtoClass: any) {
|
|
||||||
if (!parentProtoClass) return;
|
|
||||||
|
|
||||||
const field = parentProtoClass.fields.find(field => field.localName === localName);
|
|
||||||
if (!field) return;
|
|
||||||
|
|
||||||
const getter: any = Object.values(field).find(value => typeof value === "function");
|
|
||||||
return getter?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
const AppearanceSettingsProto = proxyLazy(() => searchProtoClass("appearance", PreloadedUserSettingsProtoHandler.ProtoClass));
|
|
||||||
const ClientThemeSettingsProto = proxyLazy(() => searchProtoClass("clientThemeSettings", AppearanceSettingsProto));
|
|
||||||
|
|
||||||
const USE_EXTERNAL_EMOJIS = 1n << 18n;
|
|
||||||
const USE_EXTERNAL_STICKERS = 1n << 37n;
|
|
||||||
|
|
||||||
enum EmojiIntentions {
|
|
||||||
REACTION = 0,
|
|
||||||
STATUS = 1,
|
|
||||||
COMMUNITY_CONTENT = 2,
|
|
||||||
CHAT = 3,
|
|
||||||
GUILD_STICKER_RELATED_EMOJI = 4,
|
|
||||||
GUILD_ROLE_BENEFIT_EMOJI = 5,
|
|
||||||
COMMUNITY_CONTENT_ONLY = 6,
|
|
||||||
SOUNDBOARD = 7
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BaseSticker {
|
|
||||||
available: boolean;
|
|
||||||
description: string;
|
|
||||||
format_type: number;
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
tags: string;
|
|
||||||
type: number;
|
|
||||||
}
|
|
||||||
interface GuildSticker extends BaseSticker {
|
|
||||||
guild_id: string;
|
|
||||||
}
|
|
||||||
interface DiscordSticker extends BaseSticker {
|
|
||||||
pack_id: string;
|
|
||||||
}
|
|
||||||
type Sticker = GuildSticker | DiscordSticker;
|
|
||||||
|
|
||||||
interface StickerPack {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
sku_id: string;
|
|
||||||
description: string;
|
|
||||||
cover_sticker_id: string;
|
|
||||||
banner_asset_id: string;
|
|
||||||
stickers: Sticker[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const fakeNitroEmojiRegex = /\/emojis\/(\d+?)\.(png|webp|gif)/;
|
|
||||||
const fakeNitroStickerRegex = /\/stickers\/(\d+?)\./;
|
|
||||||
const fakeNitroGifStickerRegex = /\/attachments\/\d+?\/\d+?\/(\d+?)\.gif/;
|
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
|
||||||
enableEmojiBypass: {
|
|
||||||
description: "Allow sending fake emojis",
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
default: true,
|
|
||||||
restartNeeded: true
|
|
||||||
},
|
|
||||||
emojiSize: {
|
|
||||||
description: "Size of the emojis when sending",
|
|
||||||
type: OptionType.SLIDER,
|
|
||||||
default: 48,
|
|
||||||
markers: [32, 48, 64, 128, 160, 256, 512]
|
|
||||||
},
|
|
||||||
transformEmojis: {
|
|
||||||
description: "Whether to transform fake emojis into real ones",
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
default: true,
|
|
||||||
restartNeeded: true
|
|
||||||
},
|
|
||||||
enableStickerBypass: {
|
|
||||||
description: "Allow sending fake stickers",
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
default: true,
|
|
||||||
restartNeeded: true
|
|
||||||
},
|
|
||||||
stickerSize: {
|
|
||||||
description: "Size of the stickers when sending",
|
|
||||||
type: OptionType.SLIDER,
|
|
||||||
default: 160,
|
|
||||||
markers: [32, 64, 128, 160, 256, 512]
|
|
||||||
},
|
|
||||||
transformStickers: {
|
|
||||||
description: "Whether to transform fake stickers into real ones",
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
default: true,
|
|
||||||
restartNeeded: true
|
|
||||||
},
|
|
||||||
transformCompoundSentence: {
|
|
||||||
description: "Whether to transform fake stickers and emojis in compound sentences (sentences with more content than just the fake emoji or sticker link)",
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
enableStreamQualityBypass: {
|
|
||||||
description: "Allow streaming in nitro quality",
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
default: true,
|
|
||||||
restartNeeded: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
migratePluginSettings("FakeNitro", "NitroBypass");
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "FakeNitro",
|
|
||||||
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.obscurity, Devs.captain, Devs.Nuckyz, Devs.AutumnVN],
|
|
||||||
description: "Allows you to stream in nitro quality, send fake emojis/stickers and use client themes.",
|
|
||||||
dependencies: ["MessageEventsAPI"],
|
|
||||||
|
|
||||||
settings,
|
|
||||||
|
|
||||||
patches: [
|
|
||||||
{
|
|
||||||
find: ".PREMIUM_LOCKED;",
|
|
||||||
predicate: () => settings.store.enableEmojiBypass,
|
|
||||||
replacement: [
|
|
||||||
{
|
|
||||||
match: /(?<=(\i)=\i\.intention)/,
|
|
||||||
replace: (_, intention) => `,fakeNitroIntention=${intention}`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
match: /\.(?:canUseEmojisEverywhere|canUseAnimatedEmojis)\(\i(?=\))/g,
|
|
||||||
replace: '$&,typeof fakeNitroIntention!=="undefined"?fakeNitroIntention:void 0'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
match: /(&&!\i&&)!(\i)(?=\)return \i\.\i\.DISALLOW_EXTERNAL;)/,
|
|
||||||
replace: (_, rest, canUseExternal) => `${rest}(!${canUseExternal}&&(typeof fakeNitroIntention==="undefined"||![${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)))`
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: "canUseAnimatedEmojis:function",
|
|
||||||
predicate: () => settings.store.enableEmojiBypass,
|
|
||||||
replacement: {
|
|
||||||
match: /((?:canUseEmojisEverywhere|canUseAnimatedEmojis):function\(\i)\){(.+?\))/g,
|
|
||||||
replace: (_, rest, premiumCheck) => `${rest},fakeNitroIntention){${premiumCheck}||fakeNitroIntention==null||[${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: "canUseStickersEverywhere:function",
|
|
||||||
predicate: () => settings.store.enableStickerBypass,
|
|
||||||
replacement: {
|
|
||||||
match: /canUseStickersEverywhere:function\(\i\){/,
|
|
||||||
replace: "$&return true;"
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: "\"SENDABLE\"",
|
|
||||||
predicate: () => settings.store.enableStickerBypass,
|
|
||||||
replacement: {
|
|
||||||
match: /(\w+)\.available\?/,
|
|
||||||
replace: "true?"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: "canStreamHighQuality:function",
|
|
||||||
predicate: () => settings.store.enableStreamQualityBypass,
|
|
||||||
replacement: [
|
|
||||||
"canUseHighVideoUploadQuality",
|
|
||||||
"canStreamHighQuality",
|
|
||||||
"canStreamMidQuality"
|
|
||||||
].map(func => {
|
|
||||||
return {
|
|
||||||
match: new RegExp(`${func}:function\\(\\i\\){`),
|
|
||||||
replace: "$&return true;"
|
|
||||||
};
|
|
||||||
})
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: "STREAM_FPS_OPTION.format",
|
|
||||||
predicate: () => settings.store.enableStreamQualityBypass,
|
|
||||||
replacement: {
|
|
||||||
match: /(userPremiumType|guildPremiumTier):.{0,10}TIER_\d,?/g,
|
|
||||||
replace: ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: "canUseClientThemes:function",
|
|
||||||
replacement: {
|
|
||||||
match: /canUseClientThemes:function\(\i\){/,
|
|
||||||
replace: "$&return true;"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: '.displayName="UserSettingsProtoStore"',
|
|
||||||
replacement: [
|
|
||||||
{
|
|
||||||
match: /CONNECTION_OPEN:function\((\i)\){/,
|
|
||||||
replace: (m, props) => `${m}$self.handleProtoChange(${props}.userSettingsProto,${props}.user);`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
match: /=(\i)\.local;/,
|
|
||||||
replace: (m, props) => `${m}${props}.local||$self.handleProtoChange(${props}.settings.proto);`
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: "updateTheme:function",
|
|
||||||
replacement: {
|
|
||||||
match: /(function \i\(\i\){var (\i)=\i\.backgroundGradientPresetId.+?)(\i\.\i\.updateAsync.+?theme=(.+?);.+?\),\i\))/,
|
|
||||||
replace: (_, rest, backgroundGradientPresetId, originalCall, theme) => `${rest}$self.handleGradientThemeSelect(${backgroundGradientPresetId},${theme},()=>${originalCall});`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: '["strong","em","u","text","inlineCode","s","spoiler"]',
|
|
||||||
replacement: [
|
|
||||||
{
|
|
||||||
predicate: () => settings.store.transformEmojis,
|
|
||||||
match: /1!==(\i)\.length\|\|1!==\i\.length/,
|
|
||||||
replace: (m, content) => `${m}||$self.shouldKeepEmojiLink(${content}[0])`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
predicate: () => settings.store.transformEmojis || settings.store.transformStickers,
|
|
||||||
match: /(?=return{hasSpoilerEmbeds:\i,content:(\i)})/,
|
|
||||||
replace: (_, content) => `${content}=$self.patchFakeNitroEmojisOrRemoveStickersLinks(${content},arguments[2]?.formatInline);`
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: "renderEmbeds=function",
|
|
||||||
replacement: [
|
|
||||||
{
|
|
||||||
predicate: () => settings.store.transformEmojis || settings.store.transformStickers,
|
|
||||||
match: /(renderEmbeds=function\((\i)\){)(.+?embeds\.map\(\(function\((\i)\){)/,
|
|
||||||
replace: (_, rest1, message, rest2, embed) => `${rest1}const fakeNitroMessage=${message};${rest2}if($self.shouldIgnoreEmbed(${embed},fakeNitroMessage))return null;`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
predicate: () => settings.store.transformStickers,
|
|
||||||
match: /renderStickersAccessories=function\((\i)\){var (\i)=\(0,\i\.\i\)\(\i\),/,
|
|
||||||
replace: (m, message, stickers) => `${m}${stickers}=$self.patchFakeNitroStickers(${stickers},${message}),`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
predicate: () => settings.store.transformStickers,
|
|
||||||
match: /renderAttachments=function\(\i\){var (\i)=\i.attachments.+?;/,
|
|
||||||
replace: (m, attachments) => `${m}${attachments}=$self.filterAttachments(${attachments});`
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: ".STICKER_IN_MESSAGE_HOVER,",
|
|
||||||
predicate: () => settings.store.transformStickers,
|
|
||||||
replacement: [
|
|
||||||
{
|
|
||||||
match: /var (\i)=\i\.renderableSticker,.{0,50}closePopout.+?channel:\i,closePopout:\i,/,
|
|
||||||
replace: (m, renderableSticker) => `${m}renderableSticker:${renderableSticker},`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
match: /(emojiSection.{0,50}description:)(\i)(?<=(\i)\.sticker,.+?)(?=,)/,
|
|
||||||
replace: (_, rest, reactNode, props) => `${rest}$self.addFakeNotice("STICKER",${reactNode},!!${props}.renderableSticker?.fake)`
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: ".Messages.EMOJI_POPOUT_PREMIUM_JOINED_GUILD_DESCRIPTION",
|
|
||||||
predicate: () => settings.store.transformEmojis,
|
|
||||||
replacement: {
|
|
||||||
match: /((\i)=\i\.node,\i=\i\.emojiSourceDiscoverableGuild)(.+?return )(.{0,450}Messages\.EMOJI_POPOUT_PREMIUM_JOINED_GUILD_DESCRIPTION.+?}\))/,
|
|
||||||
replace: (_, rest1, node, rest2, reactNode) => `${rest1},fakeNitroNode=${node}${rest2}$self.addFakeNotice("EMOJI",${reactNode},fakeNitroNode.fake)`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
get guildId() {
|
|
||||||
return getCurrentGuild()?.id;
|
|
||||||
},
|
|
||||||
|
|
||||||
get canUseEmotes() {
|
|
||||||
return (UserStore.getCurrentUser().premiumType ?? 0) > 0;
|
|
||||||
},
|
|
||||||
|
|
||||||
get canUseStickers() {
|
|
||||||
return (UserStore.getCurrentUser().premiumType ?? 0) > 1;
|
|
||||||
},
|
|
||||||
|
|
||||||
handleProtoChange(proto: any, user: any) {
|
|
||||||
if ((!proto.appearance && !AppearanceSettingsProto) || !UserSettingsProtoStore) return;
|
|
||||||
|
|
||||||
const premiumType: number = user?.premium_type ?? UserStore?.getCurrentUser()?.premiumType ?? 0;
|
|
||||||
|
|
||||||
if (premiumType !== 2) {
|
|
||||||
proto.appearance ??= AppearanceSettingsProto.create();
|
|
||||||
|
|
||||||
if (UserSettingsProtoStore.settings.appearance?.theme != null) {
|
|
||||||
proto.appearance.theme = UserSettingsProtoStore.settings.appearance.theme;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (UserSettingsProtoStore.settings.appearance?.clientThemeSettings?.backgroundGradientPresetId?.value != null && ClientThemeSettingsProto) {
|
|
||||||
const clientThemeSettingsDummyProto = ClientThemeSettingsProto.create({
|
|
||||||
backgroundGradientPresetId: {
|
|
||||||
value: UserSettingsProtoStore.settings.appearance.clientThemeSettings.backgroundGradientPresetId.value
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
proto.appearance.clientThemeSettings ??= clientThemeSettingsDummyProto;
|
|
||||||
proto.appearance.clientThemeSettings.backgroundGradientPresetId = clientThemeSettingsDummyProto.backgroundGradientPresetId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
handleGradientThemeSelect(backgroundGradientPresetId: number | undefined, theme: number, original: () => void) {
|
|
||||||
const premiumType = UserStore?.getCurrentUser()?.premiumType ?? 0;
|
|
||||||
if (premiumType === 2 || backgroundGradientPresetId == null) return original();
|
|
||||||
|
|
||||||
if (!AppearanceSettingsProto || !ClientThemeSettingsProto || !ReaderFactory) return;
|
|
||||||
|
|
||||||
const currentAppearanceProto = PreloadedUserSettingsProtoHandler.getCurrentValue().appearance;
|
|
||||||
|
|
||||||
const newAppearanceProto = currentAppearanceProto != null
|
|
||||||
? AppearanceSettingsProto.fromBinary(AppearanceSettingsProto.toBinary(currentAppearanceProto), ReaderFactory)
|
|
||||||
: AppearanceSettingsProto.create();
|
|
||||||
|
|
||||||
newAppearanceProto.theme = theme;
|
|
||||||
|
|
||||||
const clientThemeSettingsDummyProto = ClientThemeSettingsProto.create({
|
|
||||||
backgroundGradientPresetId: {
|
|
||||||
value: backgroundGradientPresetId
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
newAppearanceProto.clientThemeSettings ??= clientThemeSettingsDummyProto;
|
|
||||||
newAppearanceProto.clientThemeSettings.backgroundGradientPresetId = clientThemeSettingsDummyProto.backgroundGradientPresetId;
|
|
||||||
|
|
||||||
const proto = PreloadedUserSettingsProtoHandler.ProtoClass.create();
|
|
||||||
proto.appearance = newAppearanceProto;
|
|
||||||
|
|
||||||
FluxDispatcher.dispatch({
|
|
||||||
type: "USER_SETTINGS_PROTO_UPDATE",
|
|
||||||
local: true,
|
|
||||||
partial: true,
|
|
||||||
settings: {
|
|
||||||
type: 1,
|
|
||||||
proto
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
patchFakeNitroEmojisOrRemoveStickersLinks(content: Array<any>, inline: boolean) {
|
|
||||||
if (content.length > 1 && !settings.store.transformCompoundSentence) return content;
|
|
||||||
|
|
||||||
const newContent: Array<any> = [];
|
|
||||||
|
|
||||||
let nextIndex = content.length;
|
|
||||||
|
|
||||||
for (const element of content) {
|
|
||||||
if (element.props?.trusted == null) {
|
|
||||||
newContent.push(element);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settings.store.transformEmojis) {
|
|
||||||
const fakeNitroMatch = element.props.href.match(fakeNitroEmojiRegex);
|
|
||||||
if (fakeNitroMatch) {
|
|
||||||
let url: URL | null = null;
|
|
||||||
try {
|
|
||||||
url = new URL(element.props.href);
|
|
||||||
} catch { }
|
|
||||||
|
|
||||||
const emojiName = EmojiStore.getCustomEmojiById(fakeNitroMatch[1])?.name ?? url?.searchParams.get("name") ?? "FakeNitroEmoji";
|
|
||||||
|
|
||||||
newContent.push(Parser.defaultRules.customEmoji.react({
|
|
||||||
jumboable: !inline && content.length === 1,
|
|
||||||
animated: fakeNitroMatch[2] === "gif",
|
|
||||||
emojiId: fakeNitroMatch[1],
|
|
||||||
name: emojiName,
|
|
||||||
fake: true
|
|
||||||
}, void 0, { key: String(nextIndex++) }));
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settings.store.transformStickers) {
|
|
||||||
if (fakeNitroStickerRegex.test(element.props.href)) continue;
|
|
||||||
|
|
||||||
const gifMatch = element.props.href.match(fakeNitroGifStickerRegex);
|
|
||||||
if (gifMatch) {
|
|
||||||
// There is no way to differentiate a regular gif attachment from a fake nitro animated sticker, so we check if the StickerStore contains the id of the fake sticker
|
|
||||||
if (StickerStore.getStickerById(gifMatch[1])) continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
newContent.push(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
const firstContent = newContent[0];
|
|
||||||
if (typeof firstContent === "string") newContent[0] = firstContent.trimStart();
|
|
||||||
|
|
||||||
return newContent;
|
|
||||||
},
|
|
||||||
|
|
||||||
patchFakeNitroStickers(stickers: Array<any>, message: Message) {
|
|
||||||
const itemsToMaybePush: Array<string> = [];
|
|
||||||
|
|
||||||
const contentItems = message.content.split(/\s/);
|
|
||||||
if (contentItems.length === 1 && !settings.store.transformCompoundSentence) itemsToMaybePush.push(contentItems[0]);
|
|
||||||
else itemsToMaybePush.push(...contentItems);
|
|
||||||
|
|
||||||
itemsToMaybePush.push(...message.attachments.filter(attachment => attachment.content_type === "image/gif").map(attachment => attachment.url));
|
|
||||||
|
|
||||||
for (const item of itemsToMaybePush) {
|
|
||||||
const imgMatch = item.match(fakeNitroStickerRegex);
|
|
||||||
if (imgMatch) {
|
|
||||||
let url: URL | null = null;
|
|
||||||
try {
|
|
||||||
url = new URL(item);
|
|
||||||
} catch { }
|
|
||||||
|
|
||||||
const stickerName = StickerStore.getStickerById(imgMatch[1])?.name ?? url?.searchParams.get("name") ?? "FakeNitroSticker";
|
|
||||||
stickers.push({
|
|
||||||
format_type: 1,
|
|
||||||
id: imgMatch[1],
|
|
||||||
name: stickerName,
|
|
||||||
fake: true
|
|
||||||
});
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const gifMatch = item.match(fakeNitroGifStickerRegex);
|
|
||||||
if (gifMatch) {
|
|
||||||
if (!StickerStore.getStickerById(gifMatch[1])) continue;
|
|
||||||
|
|
||||||
const stickerName = StickerStore.getStickerById(gifMatch[1])?.name ?? "FakeNitroSticker";
|
|
||||||
stickers.push({
|
|
||||||
format_type: 2,
|
|
||||||
id: gifMatch[1],
|
|
||||||
name: stickerName,
|
|
||||||
fake: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return stickers;
|
|
||||||
},
|
|
||||||
|
|
||||||
shouldIgnoreEmbed(embed: Message["embeds"][number], message: Message) {
|
|
||||||
if (message.content.split(/\s/).length > 1 && !settings.store.transformCompoundSentence) return false;
|
|
||||||
|
|
||||||
switch (embed.type) {
|
|
||||||
case "image": {
|
|
||||||
if (settings.store.transformEmojis) {
|
|
||||||
if (fakeNitroEmojiRegex.test(embed.url!)) return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settings.store.transformStickers) {
|
|
||||||
if (fakeNitroStickerRegex.test(embed.url!)) return true;
|
|
||||||
|
|
||||||
const gifMatch = embed.url!.match(fakeNitroGifStickerRegex);
|
|
||||||
if (gifMatch) {
|
|
||||||
// There is no way to differentiate a regular gif attachment from a fake nitro animated sticker, so we check if the StickerStore contains the id of the fake sticker
|
|
||||||
if (StickerStore.getStickerById(gifMatch[1])) return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
filterAttachments(attachments: Message["attachments"]) {
|
|
||||||
return attachments.filter(attachment => {
|
|
||||||
if (attachment.content_type !== "image/gif") return true;
|
|
||||||
|
|
||||||
const match = attachment.url.match(fakeNitroGifStickerRegex);
|
|
||||||
if (match) {
|
|
||||||
// There is no way to differentiate a regular gif attachment from a fake nitro animated sticker, so we check if the StickerStore contains the id of the fake sticker
|
|
||||||
if (StickerStore.getStickerById(match[1])) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
shouldKeepEmojiLink(link: any) {
|
|
||||||
return link.target && fakeNitroEmojiRegex.test(link.target);
|
|
||||||
},
|
|
||||||
|
|
||||||
addFakeNotice(type: "STICKER" | "EMOJI", node: Array<ReactNode>, fake: boolean) {
|
|
||||||
if (!fake) return node;
|
|
||||||
|
|
||||||
node = Array.isArray(node) ? node : [node];
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case "STICKER": {
|
|
||||||
node.push(" This is a FakeNitro sticker and renders like a real sticker only for you. Appears as a link to non-plugin users.");
|
|
||||||
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
case "EMOJI": {
|
|
||||||
node.push(" This is a FakeNitro emoji and renders like a real emoji only for you. Appears as a link to non-plugin users.");
|
|
||||||
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
hasPermissionToUseExternalEmojis(channelId: string) {
|
|
||||||
const channel = ChannelStore.getChannel(channelId);
|
|
||||||
|
|
||||||
if (!channel || channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM()) return true;
|
|
||||||
|
|
||||||
return PermissionStore.can(USE_EXTERNAL_EMOJIS, channel);
|
|
||||||
},
|
|
||||||
|
|
||||||
hasPermissionToUseExternalStickers(channelId: string) {
|
|
||||||
const channel = ChannelStore.getChannel(channelId);
|
|
||||||
|
|
||||||
if (!channel || channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM()) return true;
|
|
||||||
|
|
||||||
return PermissionStore.can(USE_EXTERNAL_STICKERS, channel);
|
|
||||||
},
|
|
||||||
|
|
||||||
getStickerLink(stickerId: string) {
|
|
||||||
return `https://media.discordapp.net/stickers/${stickerId}.png?size=${Settings.plugins.FakeNitro.stickerSize}`;
|
|
||||||
},
|
|
||||||
|
|
||||||
async sendAnimatedSticker(stickerLink: string, stickerId: string, channelId: string) {
|
|
||||||
const [{ parseURL }, {
|
|
||||||
GIFEncoder,
|
|
||||||
quantize,
|
|
||||||
applyPalette
|
|
||||||
}] = await Promise.all([importApngJs(), getGifEncoder()]);
|
|
||||||
|
|
||||||
const { frames, width, height } = await parseURL(stickerLink);
|
|
||||||
|
|
||||||
const gif = new GIFEncoder();
|
|
||||||
const resolution = Settings.plugins.FakeNitro.stickerSize;
|
|
||||||
|
|
||||||
const canvas = document.createElement("canvas");
|
|
||||||
canvas.width = resolution;
|
|
||||||
canvas.height = resolution;
|
|
||||||
|
|
||||||
const ctx = canvas.getContext("2d", {
|
|
||||||
willReadFrequently: true
|
|
||||||
})!;
|
|
||||||
|
|
||||||
const scale = resolution / Math.max(width, height);
|
|
||||||
ctx.scale(scale, scale);
|
|
||||||
|
|
||||||
let previousFrameData: ImageData;
|
|
||||||
|
|
||||||
for (const frame of frames) {
|
|
||||||
const { left, top, width, height, img, delay, blendOp, disposeOp } = frame;
|
|
||||||
|
|
||||||
previousFrameData = ctx.getImageData(left, top, width, height);
|
|
||||||
|
|
||||||
if (blendOp === ApngBlendOp.SOURCE) {
|
|
||||||
ctx.clearRect(left, top, width, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.drawImage(img, left, top, width, height);
|
|
||||||
|
|
||||||
const { data } = ctx.getImageData(0, 0, resolution, resolution);
|
|
||||||
|
|
||||||
const palette = quantize(data, 256);
|
|
||||||
const index = applyPalette(data, palette);
|
|
||||||
|
|
||||||
gif.writeFrame(index, resolution, resolution, {
|
|
||||||
transparent: true,
|
|
||||||
palette,
|
|
||||||
delay
|
|
||||||
});
|
|
||||||
|
|
||||||
if (disposeOp === ApngDisposeOp.BACKGROUND) {
|
|
||||||
ctx.clearRect(left, top, width, height);
|
|
||||||
} else if (disposeOp === ApngDisposeOp.PREVIOUS) {
|
|
||||||
ctx.putImageData(previousFrameData, left, top);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
gif.finish();
|
|
||||||
|
|
||||||
const file = new File([gif.bytesView()], `${stickerId}.gif`, { type: "image/gif" });
|
|
||||||
promptToUpload([file], ChannelStore.getChannel(channelId), DRAFT_TYPE);
|
|
||||||
},
|
|
||||||
|
|
||||||
start() {
|
|
||||||
const settings = Settings.plugins.FakeNitro;
|
|
||||||
if (!settings.enableEmojiBypass && !settings.enableStickerBypass) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWordBoundary(origStr: string, offset: number) {
|
|
||||||
return (!origStr[offset] || /\s/.test(origStr[offset])) ? "" : " ";
|
|
||||||
}
|
|
||||||
|
|
||||||
this.preSend = addPreSendListener((channelId, messageObj, extra) => {
|
|
||||||
const { guildId } = this;
|
|
||||||
|
|
||||||
stickerBypass: {
|
|
||||||
if (!settings.enableStickerBypass)
|
|
||||||
break stickerBypass;
|
|
||||||
|
|
||||||
const sticker = StickerStore.getStickerById(extra?.stickerIds?.[0]!);
|
|
||||||
if (!sticker)
|
|
||||||
break stickerBypass;
|
|
||||||
|
|
||||||
if (sticker.available !== false && ((this.canUseStickers && this.hasPermissionToUseExternalStickers(channelId)) || (sticker as GuildSticker)?.guild_id === guildId))
|
|
||||||
break stickerBypass;
|
|
||||||
|
|
||||||
let link = this.getStickerLink(sticker.id);
|
|
||||||
if (sticker.format_type === 2) {
|
|
||||||
this.sendAnimatedSticker(link, sticker.id, channelId);
|
|
||||||
return { cancel: true };
|
|
||||||
} else {
|
|
||||||
if ("pack_id" in sticker) {
|
|
||||||
const packId = sticker.pack_id === "847199849233514549"
|
|
||||||
// Discord moved these stickers into a different pack at some point, but
|
|
||||||
// Distok still uses the old id
|
|
||||||
? "749043879713701898"
|
|
||||||
: sticker.pack_id;
|
|
||||||
|
|
||||||
link = `https://distok.top/stickers/${packId}/${sticker.id}.gif`;
|
|
||||||
}
|
|
||||||
|
|
||||||
delete extra.stickerIds;
|
|
||||||
messageObj.content += " " + link + `&name=${encodeURIComponent(sticker.name)}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((!this.canUseEmotes || !this.hasPermissionToUseExternalEmojis(channelId)) && settings.enableEmojiBypass) {
|
|
||||||
for (const emoji of messageObj.validNonShortcutEmojis) {
|
|
||||||
if (!emoji.require_colons) continue;
|
|
||||||
if (emoji.guildId === guildId && !emoji.animated) continue;
|
|
||||||
|
|
||||||
const emojiString = `<${emoji.animated ? "a" : ""}:${emoji.originalName || emoji.name}:${emoji.id}>`;
|
|
||||||
const url = emoji.url.replace(/\?size=\d+/, "?" + new URLSearchParams({
|
|
||||||
size: Settings.plugins.FakeNitro.emojiSize,
|
|
||||||
name: encodeURIComponent(emoji.name)
|
|
||||||
}));
|
|
||||||
messageObj.content = messageObj.content.replace(emojiString, (match, offset, origStr) => {
|
|
||||||
return `${getWordBoundary(origStr, offset - 1)}${url}${getWordBoundary(origStr, offset + match.length)}`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { cancel: false };
|
|
||||||
});
|
|
||||||
|
|
||||||
this.preEdit = addPreEditListener((channelId, __, messageObj) => {
|
|
||||||
if (this.canUseEmotes && this.hasPermissionToUseExternalEmojis(channelId)) return;
|
|
||||||
|
|
||||||
const { guildId } = this;
|
|
||||||
|
|
||||||
for (const [emojiStr, _, emojiId] of messageObj.content.matchAll(/(?<!\\)<a?:(\w+):(\d+)>/ig)) {
|
|
||||||
const emoji = EmojiStore.getCustomEmojiById(emojiId);
|
|
||||||
if (emoji == null || (emoji.guildId === guildId && !emoji.animated)) continue;
|
|
||||||
if (!emoji.require_colons) continue;
|
|
||||||
|
|
||||||
const url = emoji.url.replace(/\?size=\d+/, "?" + new URLSearchParams({
|
|
||||||
size: Settings.plugins.FakeNitro.emojiSize,
|
|
||||||
name: encodeURIComponent(emoji.name)
|
|
||||||
}));
|
|
||||||
messageObj.content = messageObj.content.replace(emojiStr, (match, offset, origStr) => {
|
|
||||||
return `${getWordBoundary(origStr, offset - 1)}${url}${getWordBoundary(origStr, offset + match.length)}`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
removePreSendListener(this.preSend);
|
|
||||||
removePreEditListener(this.preEdit);
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,145 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2023 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// This plugin is a port from Alyxia's Vendetta plugin
|
|
||||||
import { definePluginSettings } from "@api/settings";
|
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
|
||||||
import { Devs } from "@utils/constants";
|
|
||||||
import { Margins } from "@utils/margins";
|
|
||||||
import { copyWithToast } from "@utils/misc";
|
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
|
||||||
import { Button, Forms } from "@webpack/common";
|
|
||||||
import { User } from "discord-types/general";
|
|
||||||
import virtualMerge from "virtual-merge";
|
|
||||||
|
|
||||||
interface UserProfile extends User {
|
|
||||||
themeColors?: Array<number>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Colors {
|
|
||||||
primary: number;
|
|
||||||
accent: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
function encode(primary: number, accent: number): string {
|
|
||||||
const message = `[#${primary.toString(16).padStart(6, "0")},#${accent.toString(16).padStart(6, "0")}]`;
|
|
||||||
const padding = "";
|
|
||||||
const encoded = Array.from(message)
|
|
||||||
.map(x => x.codePointAt(0))
|
|
||||||
.filter(x => x! >= 0x20 && x! <= 0x7f)
|
|
||||||
.map(x => String.fromCodePoint(x! + 0xe0000))
|
|
||||||
.join("");
|
|
||||||
|
|
||||||
return (padding || "") + " " + encoded;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Courtesy of Cynthia.
|
|
||||||
function decode(bio: string): Array<number> | null {
|
|
||||||
if (bio == null) return null;
|
|
||||||
|
|
||||||
const colorString = bio.match(
|
|
||||||
/\u{e005b}\u{e0023}([\u{e0061}-\u{e0066}\u{e0041}-\u{e0046}\u{e0030}-\u{e0039}]+?)\u{e002c}\u{e0023}([\u{e0061}-\u{e0066}\u{e0041}-\u{e0046}\u{e0030}-\u{e0039}]+?)\u{e005d}/u,
|
|
||||||
);
|
|
||||||
if (colorString != null) {
|
|
||||||
const parsed = [...colorString[0]]
|
|
||||||
.map(x => String.fromCodePoint(x.codePointAt(0)! - 0xe0000))
|
|
||||||
.join("");
|
|
||||||
const colors = parsed
|
|
||||||
.substring(1, parsed.length - 1)
|
|
||||||
.split(",")
|
|
||||||
.map(x => parseInt(x.replace("#", "0x"), 16));
|
|
||||||
|
|
||||||
return colors;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
|
||||||
nitroFirst: {
|
|
||||||
description: "Default color source if both are present",
|
|
||||||
type: OptionType.SELECT,
|
|
||||||
options: [
|
|
||||||
{ label: "Nitro colors", value: true, default: true },
|
|
||||||
{ label: "Fake colors", value: false },
|
|
||||||
]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "FakeProfileThemes",
|
|
||||||
description: "Allows profile theming by hiding the colors in your bio thanks to invisible 3y3 encoding",
|
|
||||||
authors: [Devs.Alyxia, Devs.Remty],
|
|
||||||
patches: [
|
|
||||||
{
|
|
||||||
find: "getUserProfile=",
|
|
||||||
replacement: {
|
|
||||||
match: /(?<=getUserProfile=function\(\i\){return )(\i\[\i\])/,
|
|
||||||
replace: "$self.colorDecodeHook($1)"
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
find: ".USER_SETTINGS_PROFILE_THEME_ACCENT",
|
|
||||||
replacement: {
|
|
||||||
match: /RESET_PROFILE_THEME}\)(?<=},color:(\i).+?},color:(\i).+?)/,
|
|
||||||
replace: "$&,$self.addCopy3y3Button({primary:$1,accent:$2})"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
settingsAboutComponent: () => (
|
|
||||||
<Forms.FormSection>
|
|
||||||
<Forms.FormTitle tag="h3">Usage</Forms.FormTitle>
|
|
||||||
<Forms.FormText>
|
|
||||||
After enabling this plugin, you will see custom colors in the profiles of other people using compatible plugins. <br />
|
|
||||||
To set your own colors:
|
|
||||||
<ul>
|
|
||||||
<li>• go to your profile settings</li>
|
|
||||||
<li>• choose your own colors in the Nitro preview</li>
|
|
||||||
<li>• click the "Copy 3y3" button</li>
|
|
||||||
<li>• paste the invisible text anywhere in your bio</li>
|
|
||||||
</ul><br />
|
|
||||||
<b>Please note:</b> if you are using a theme which hides nitro ads, you should disable it temporarily to set colors.
|
|
||||||
</Forms.FormText>
|
|
||||||
</Forms.FormSection>),
|
|
||||||
settings,
|
|
||||||
colorDecodeHook(user: UserProfile) {
|
|
||||||
if (user) {
|
|
||||||
// don't replace colors if already set with nitro
|
|
||||||
if (settings.store.nitroFirst && user.themeColors) return user;
|
|
||||||
const colors = decode(user.bio);
|
|
||||||
if (colors) {
|
|
||||||
return virtualMerge(user, {
|
|
||||||
premiumType: 2,
|
|
||||||
themeColors: colors
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return user;
|
|
||||||
},
|
|
||||||
addCopy3y3Button: ErrorBoundary.wrap(function ({ primary, accent }: Colors) {
|
|
||||||
return <Button
|
|
||||||
onClick={() => {
|
|
||||||
const colorString = encode(primary, accent);
|
|
||||||
copyWithToast(colorString);
|
|
||||||
}}
|
|
||||||
color={Button.Colors.PRIMARY}
|
|
||||||
size={Button.Sizes.XLARGE}
|
|
||||||
className={Margins.left16}
|
|
||||||
>Copy 3y3
|
|
||||||
</Button >;
|
|
||||||
}, { noop: true }),
|
|
||||||
});
|
|
@ -1,56 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2023 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Devs } from "@utils/constants";
|
|
||||||
import definePlugin from "@utils/types";
|
|
||||||
import { Forms } from "@webpack/common";
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "FixInbox",
|
|
||||||
description: "Fixes the Unreads Inbox from crashing Discord when you're in lots of guilds.",
|
|
||||||
authors: [Devs.Megu],
|
|
||||||
|
|
||||||
patches: [{
|
|
||||||
find: "INBOX_OPEN:function",
|
|
||||||
replacement: {
|
|
||||||
// This function normally dispatches a subscribe event to every guild.
|
|
||||||
// this is badbadbadbadbad so we just get rid of it.
|
|
||||||
match: /INBOX_OPEN:function.+?\{/,
|
|
||||||
replace: "$&return true;"
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
|
|
||||||
settingsAboutComponent() {
|
|
||||||
return (
|
|
||||||
<Forms.FormSection>
|
|
||||||
<Forms.FormTitle tag="h3">What's the problem?</Forms.FormTitle>
|
|
||||||
<Forms.FormText style={{ marginBottom: 8 }}>
|
|
||||||
By default, Discord emits a GUILD_SUBSCRIPTIONS event for every guild you're in.
|
|
||||||
When you're in a lot of guilds, this can cause the gateway to ratelimit you.
|
|
||||||
This causes the client to crash and get stuck in an infinite ratelimit loop as it tries to reconnect.
|
|
||||||
</Forms.FormText>
|
|
||||||
|
|
||||||
<Forms.FormTitle tag="h3">How does it work?</Forms.FormTitle>
|
|
||||||
<Forms.FormText>
|
|
||||||
This plugin works by stopping the client from sending GUILD_SUBSCRIPTIONS events to the gateway when you open the unreads inbox.
|
|
||||||
This means that not all unreads will be shown, instead only already-subscribed guilds' unreads will be shown, but your client won't crash anymore.
|
|
||||||
</Forms.FormText>
|
|
||||||
</Forms.FormSection>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
@ -19,16 +19,12 @@
|
|||||||
import { ApplicationCommandInputType, sendBotMessage } from "@api/Commands";
|
import { ApplicationCommandInputType, sendBotMessage } from "@api/Commands";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByProps } from "@webpack";
|
||||||
import { RestAPI, UserStore } from "@webpack/common";
|
|
||||||
|
|
||||||
const FriendInvites = findByPropsLazy("createFriendInvite");
|
|
||||||
const uuid = findByPropsLazy("v4", "v1");
|
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "FriendInvites",
|
name: "FriendInvites",
|
||||||
description: "Create and manage friend invite links via slash commands (/create friend invite, /view friend invites, /revoke friend invites).",
|
description: "Generate and manage friend invite links.",
|
||||||
authors: [Devs.afn, Devs.Dziurwa],
|
authors: [Devs.afn],
|
||||||
dependencies: ["CommandsAPI"],
|
dependencies: ["CommandsAPI"],
|
||||||
commands: [
|
commands: [
|
||||||
{
|
{
|
||||||
@ -36,35 +32,14 @@ export default definePlugin({
|
|||||||
description: "Generates a friend invite link.",
|
description: "Generates a friend invite link.",
|
||||||
inputType: ApplicationCommandInputType.BOT,
|
inputType: ApplicationCommandInputType.BOT,
|
||||||
execute: async (_, ctx) => {
|
execute: async (_, ctx) => {
|
||||||
if (!UserStore.getCurrentUser().phone)
|
const friendInvites = findByProps("createFriendInvite");
|
||||||
return sendBotMessage(ctx.channel.id, {
|
const createInvite = await friendInvites.createFriendInvite();
|
||||||
content: "You need to have a phone number connected to your account to create a friend invite!"
|
|
||||||
});
|
|
||||||
|
|
||||||
const random = uuid.v4();
|
return void sendBotMessage(ctx.channel.id, {
|
||||||
const invite = await RestAPI.post({
|
|
||||||
url: "/friend-finder/find-friends",
|
|
||||||
body: {
|
|
||||||
modified_contacts: {
|
|
||||||
[random]: [1, "", ""]
|
|
||||||
},
|
|
||||||
phone_contact_methods_count: 1
|
|
||||||
}
|
|
||||||
}).then(res =>
|
|
||||||
FriendInvites.createFriendInvite({
|
|
||||||
code: res.body.invite_suggestions[0][3],
|
|
||||||
recipient_phone_number_or_email: random,
|
|
||||||
contact_visibility: 1,
|
|
||||||
filter_visibilities: [],
|
|
||||||
filtered_invite_suggestions_index: 1
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
sendBotMessage(ctx.channel.id, {
|
|
||||||
content: `
|
content: `
|
||||||
discord.gg/${invite.code} ·
|
discord.gg/${createInvite.code}
|
||||||
Expires: <t:${new Date(invite.expires_at).getTime() / 1000}:R> ·
|
Expires: <t:${new Date(createInvite.expires_at).getTime() / 1000}:R>
|
||||||
Max uses: \`${invite.max_uses}\`
|
Max uses: \`${createInvite.max_uses}\`
|
||||||
`.trim().replace(/\s+/g, " ")
|
`.trim().replace(/\s+/g, " ")
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -74,29 +49,28 @@ export default definePlugin({
|
|||||||
description: "View a list of all generated friend invites.",
|
description: "View a list of all generated friend invites.",
|
||||||
inputType: ApplicationCommandInputType.BOT,
|
inputType: ApplicationCommandInputType.BOT,
|
||||||
execute: async (_, ctx) => {
|
execute: async (_, ctx) => {
|
||||||
const invites = await FriendInvites.getAllFriendInvites();
|
const friendInvites = findByProps("createFriendInvite");
|
||||||
|
const invites = await friendInvites.getAllFriendInvites();
|
||||||
const friendInviteList = invites.map(i =>
|
const friendInviteList = invites.map(i =>
|
||||||
`
|
`_discord.gg/${i.code}_
|
||||||
_discord.gg/${i.code}_ ·
|
Expires: <t:${new Date(i.expires_at).getTime() / 1000}:R>
|
||||||
Expires: <t:${new Date(i.expires_at).getTime() / 1000}:R> ·
|
Times used: \`${i.uses}/${i.max_uses}\``.trim().replace(/\s+/g, " ")
|
||||||
Times used: \`${i.uses}/${i.max_uses}\`
|
|
||||||
`.trim().replace(/\s+/g, " ")
|
|
||||||
);
|
);
|
||||||
|
|
||||||
sendBotMessage(ctx.channel.id, {
|
return void sendBotMessage(ctx.channel.id, {
|
||||||
content: friendInviteList.join("\n") || "You have no active friend invites!"
|
content: friendInviteList.join("\n\n") || "You have no active friend invites!"
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "revoke friend invites",
|
name: "revoke friend invites",
|
||||||
description: "Revokes all generated friend invites.",
|
description: "Revokes ALL generated friend invite links.",
|
||||||
inputType: ApplicationCommandInputType.BOT,
|
inputType: ApplicationCommandInputType.BOT,
|
||||||
execute: async (_, ctx) => {
|
execute: async (_, ctx) => {
|
||||||
await FriendInvites.revokeFriendInvites();
|
await findByProps("createFriendInvite").revokeFriendInvites();
|
||||||
|
|
||||||
return void sendBotMessage(ctx.channel.id, {
|
return void sendBotMessage(ctx.channel.id, {
|
||||||
content: "All friend invites have been revoked."
|
content: "All friend links have been revoked."
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1,85 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2023 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { getSettingStoreLazy } from "@api/SettingsStore";
|
|
||||||
import { disableStyle, enableStyle } from "@api/Styles";
|
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
|
||||||
import { Devs } from "@utils/constants";
|
|
||||||
import definePlugin from "@utils/types";
|
|
||||||
import { findByCodeLazy } from "@webpack";
|
|
||||||
|
|
||||||
import style from "./style.css?managed";
|
|
||||||
|
|
||||||
const ShowCurrentGame = getSettingStoreLazy<boolean>("status", "showCurrentGame");
|
|
||||||
const Button = findByCodeLazy("Button.Sizes.NONE,disabled:");
|
|
||||||
|
|
||||||
function makeIcon(showCurrentGame?: boolean) {
|
|
||||||
return function () {
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 96 960 960"
|
|
||||||
>
|
|
||||||
<path fill="currentColor" d="M182 856q-51 0-79-35.5T82 734l42-300q9-60 53.5-99T282 296h396q60 0 104.5 39t53.5 99l42 300q7 51-21 86.5T778 856q-21 0-39-7.5T706 826l-90-90H344l-90 90q-15 15-33 22.5t-39 7.5Zm498-240q17 0 28.5-11.5T720 576q0-17-11.5-28.5T680 536q-17 0-28.5 11.5T640 576q0 17 11.5 28.5T680 616Zm-80-120q17 0 28.5-11.5T640 456q0-17-11.5-28.5T600 416q-17 0-28.5 11.5T560 456q0 17 11.5 28.5T600 496ZM310 616h60v-70h70v-60h-70v-70h-60v70h-70v60h70v70Z" />
|
|
||||||
{!showCurrentGame && <line x1="920" y1="280" x2="40" y2="880" stroke="var(--status-danger)" stroke-width="80" />}
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function GameActivityToggleButton() {
|
|
||||||
const showCurrentGame = ShowCurrentGame?.useSetting();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
tooltipText={showCurrentGame ? "Disable Game Activity" : "Enable Game Activity"}
|
|
||||||
icon={makeIcon(showCurrentGame)}
|
|
||||||
role="switch"
|
|
||||||
aria-checked={!showCurrentGame}
|
|
||||||
onClick={() => ShowCurrentGame?.updateSetting(old => !old)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "GameActivityToggle",
|
|
||||||
description: "Adds a button next to the mic and deafen button to toggle game activity.",
|
|
||||||
authors: [Devs.Nuckyz],
|
|
||||||
dependencies: ["SettingsStoreAPI"],
|
|
||||||
|
|
||||||
patches: [
|
|
||||||
{
|
|
||||||
find: ".Messages.ACCOUNT_SPEAKING_WHILE_MUTED",
|
|
||||||
replacement: {
|
|
||||||
match: /this\.renderNameZone\(\).+?children:\[/,
|
|
||||||
replace: "$&$self.GameActivityToggleButton(),"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
GameActivityToggleButton: ErrorBoundary.wrap(GameActivityToggleButton, { noop: true }),
|
|
||||||
|
|
||||||
start() {
|
|
||||||
enableStyle(style);
|
|
||||||
},
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
disableStyle(style);
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,3 +0,0 @@
|
|||||||
[class*="withTagAsButton"] {
|
|
||||||
min-width: 88px;
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2023 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Devs } from "@utils/constants";
|
|
||||||
import definePlugin from "@utils/types";
|
|
||||||
import { filters, mapMangledModuleLazy } from "@webpack";
|
|
||||||
import { ComponentDispatch } from "@webpack/common";
|
|
||||||
|
|
||||||
const ExpressionPickerState = mapMangledModuleLazy('name:"expression-picker-last-active-view"', {
|
|
||||||
close: filters.byCode("activeView:null", "setState")
|
|
||||||
});
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "GifPaste",
|
|
||||||
description: "Makes picking a gif in the gif picker insert a link into the chatbox instead of instantly sending it",
|
|
||||||
authors: [Devs.Ven],
|
|
||||||
|
|
||||||
patches: [{
|
|
||||||
find: ".handleSelectGIF=",
|
|
||||||
replacement: {
|
|
||||||
match: /\.handleSelectGIF=function.+?\{/,
|
|
||||||
replace: ".handleSelectGIF=function(gif){return $self.handleSelect(gif);"
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
|
|
||||||
handleSelect(gif?: { url: string; }) {
|
|
||||||
if (gif) {
|
|
||||||
ComponentDispatch.dispatchToLastSubscribed("INSERT_TEXT", { rawText: gif.url + " " });
|
|
||||||
ExpressionPickerState.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
@ -21,7 +21,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
|
|||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { useForceUpdater } from "@utils/misc";
|
import { useForceUpdater } from "@utils/misc";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { findByPropsLazy, findStoreLazy } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
import { Tooltip } from "webpack/common";
|
import { Tooltip } from "webpack/common";
|
||||||
|
|
||||||
enum ActivitiesTypes {
|
enum ActivitiesTypes {
|
||||||
@ -37,7 +37,7 @@ interface IgnoredActivity {
|
|||||||
const RegisteredGamesClasses = findByPropsLazy("overlayToggleIconOff", "overlayToggleIconOn");
|
const RegisteredGamesClasses = findByPropsLazy("overlayToggleIconOff", "overlayToggleIconOn");
|
||||||
const TryItOutClasses = findByPropsLazy("tryItOutBadge", "tryItOutBadgeIcon");
|
const TryItOutClasses = findByPropsLazy("tryItOutBadge", "tryItOutBadgeIcon");
|
||||||
const BaseShapeRoundClasses = findByPropsLazy("baseShapeRound", "baseShapeRoundLeft", "baseShapeRoundRight");
|
const BaseShapeRoundClasses = findByPropsLazy("baseShapeRound", "baseShapeRoundLeft", "baseShapeRoundRight");
|
||||||
const RunningGameStore = findStoreLazy("RunningGameStore");
|
const RunningGameStore = findByPropsLazy("getRunningGames", "getGamesSeen");
|
||||||
|
|
||||||
function ToggleIconOff() {
|
function ToggleIconOff() {
|
||||||
return (
|
return (
|
||||||
@ -71,7 +71,7 @@ function ToggleIconOff() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ToggleIconOn({ forceWhite }: { forceWhite?: boolean; }) {
|
function ToggleIconOn() {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
className={RegisteredGamesClasses.overlayToggleIconOn}
|
className={RegisteredGamesClasses.overlayToggleIconOn}
|
||||||
@ -80,15 +80,14 @@ function ToggleIconOn({ forceWhite }: { forceWhite?: boolean; }) {
|
|||||||
viewBox="0 0 32 26"
|
viewBox="0 0 32 26"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
className={forceWhite ? "" : RegisteredGamesClasses.fill}
|
className={RegisteredGamesClasses.fill}
|
||||||
fill={forceWhite ? "var(--white-500)" : ""}
|
|
||||||
d="M 16 8 C 7.664063 8 1.25 15.34375 1.25 15.34375 L 0.65625 16 L 1.25 16.65625 C 1.25 16.65625 7.097656 23.324219 14.875 23.9375 C 15.246094 23.984375 15.617188 24 16 24 C 16.382813 24 16.753906 23.984375 17.125 23.9375 C 24.902344 23.324219 30.75 16.65625 30.75 16.65625 L 31.34375 16 L 30.75 15.34375 C 30.75 15.34375 24.335938 8 16 8 Z M 16 10 C 18.203125 10 20.234375 10.601563 22 11.40625 C 22.636719 12.460938 23 13.675781 23 15 C 23 18.613281 20.289063 21.582031 16.78125 21.96875 C 16.761719 21.972656 16.738281 21.964844 16.71875 21.96875 C 16.480469 21.980469 16.242188 22 16 22 C 15.734375 22 15.476563 21.984375 15.21875 21.96875 C 11.710938 21.582031 9 18.613281 9 15 C 9 13.695313 9.351563 12.480469 9.96875 11.4375 L 9.9375 11.4375 C 11.71875 10.617188 13.773438 10 16 10 Z M 16 12 C 14.34375 12 13 13.34375 13 15 C 13 16.65625 14.34375 18 16 18 C 17.65625 18 19 16.65625 19 15 C 19 13.34375 17.65625 12 16 12 Z M 7.25 12.9375 C 7.09375 13.609375 7 14.285156 7 15 C 7 16.753906 7.5 18.394531 8.375 19.78125 C 5.855469 18.324219 4.105469 16.585938 3.53125 16 C 4.011719 15.507813 5.351563 14.203125 7.25 12.9375 Z M 24.75 12.9375 C 26.648438 14.203125 27.988281 15.507813 28.46875 16 C 27.894531 16.585938 26.144531 18.324219 23.625 19.78125 C 24.5 18.394531 25 16.753906 25 15 C 25 14.285156 24.90625 13.601563 24.75 12.9375 Z"
|
d="M 16 8 C 7.664063 8 1.25 15.34375 1.25 15.34375 L 0.65625 16 L 1.25 16.65625 C 1.25 16.65625 7.097656 23.324219 14.875 23.9375 C 15.246094 23.984375 15.617188 24 16 24 C 16.382813 24 16.753906 23.984375 17.125 23.9375 C 24.902344 23.324219 30.75 16.65625 30.75 16.65625 L 31.34375 16 L 30.75 15.34375 C 30.75 15.34375 24.335938 8 16 8 Z M 16 10 C 18.203125 10 20.234375 10.601563 22 11.40625 C 22.636719 12.460938 23 13.675781 23 15 C 23 18.613281 20.289063 21.582031 16.78125 21.96875 C 16.761719 21.972656 16.738281 21.964844 16.71875 21.96875 C 16.480469 21.980469 16.242188 22 16 22 C 15.734375 22 15.476563 21.984375 15.21875 21.96875 C 11.710938 21.582031 9 18.613281 9 15 C 9 13.695313 9.351563 12.480469 9.96875 11.4375 L 9.9375 11.4375 C 11.71875 10.617188 13.773438 10 16 10 Z M 16 12 C 14.34375 12 13 13.34375 13 15 C 13 16.65625 14.34375 18 16 18 C 17.65625 18 19 16.65625 19 15 C 19 13.34375 17.65625 12 16 12 Z M 7.25 12.9375 C 7.09375 13.609375 7 14.285156 7 15 C 7 16.753906 7.5 18.394531 8.375 19.78125 C 5.855469 18.324219 4.105469 16.585938 3.53125 16 C 4.011719 15.507813 5.351563 14.203125 7.25 12.9375 Z M 24.75 12.9375 C 26.648438 14.203125 27.988281 15.507813 28.46875 16 C 27.894531 16.585938 26.144531 18.324219 23.625 19.78125 C 24.5 18.394531 25 16.753906 25 15 C 25 14.285156 24.90625 13.601563 24.75 12.9375 Z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ToggleActivityComponent({ activity, forceWhite, forceLeftMargin }: { activity: IgnoredActivity; forceWhite?: boolean; forceLeftMargin?: boolean; }) {
|
function ToggleActivityComponent({ activity }: { activity: IgnoredActivity; }) {
|
||||||
const forceUpdate = useForceUpdater();
|
const forceUpdate = useForceUpdater();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -101,13 +100,12 @@ function ToggleActivityComponent({ activity, forceWhite, forceLeftMargin }: { ac
|
|||||||
role="button"
|
role="button"
|
||||||
aria-label="Toggle activity"
|
aria-label="Toggle activity"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
style={forceLeftMargin ? { marginLeft: "2px" } : undefined}
|
|
||||||
onClick={e => handleActivityToggle(e, activity, forceUpdate)}
|
onClick={e => handleActivityToggle(e, activity, forceUpdate)}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
ignoredActivitiesCache.has(activity.id)
|
ignoredActivitiesCache.has(activity.id)
|
||||||
? <ToggleIconOff />
|
? <ToggleIconOff />
|
||||||
: <ToggleIconOn forceWhite={forceWhite} />
|
: <ToggleIconOn />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -119,9 +117,9 @@ function ToggleActivityComponentWithBackground({ activity }: { activity: Ignored
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${TryItOutClasses.tryItOutBadge} ${BaseShapeRoundClasses.baseShapeRound}`}
|
className={`${TryItOutClasses.tryItOutBadge} ${BaseShapeRoundClasses.baseShapeRound}`}
|
||||||
style={{ padding: "0px 2px" }}
|
style={{ padding: "0 2px" }}
|
||||||
>
|
>
|
||||||
<ToggleActivityComponent activity={activity} forceWhite={true} />
|
<ToggleActivityComponent activity={activity} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -144,32 +142,25 @@ export default definePlugin({
|
|||||||
name: "IgnoreActivities",
|
name: "IgnoreActivities",
|
||||||
authors: [Devs.Nuckyz],
|
authors: [Devs.Nuckyz],
|
||||||
description: "Ignore certain activities (like games and actual activities) from showing up on your status. You can configure which ones are ignored from the Registered Games and Activities tabs.",
|
description: "Ignore certain activities (like games and actual activities) from showing up on your status. You can configure which ones are ignored from the Registered Games and Activities tabs.",
|
||||||
patches: [
|
patches: [{
|
||||||
{
|
|
||||||
find: ".Messages.SETTINGS_GAMES_TOGGLE_OVERLAY",
|
find: ".Messages.SETTINGS_GAMES_TOGGLE_OVERLAY",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /!(\i)(\)return null;var \i=(\i)\.overlay.+?children:)(\[.{0,70}overlayStatusText.+?\])(?=}\)}\(\))/,
|
match: /var .=(?<props>.)\.overlay.+?"aria-label":.\..\.Messages\.SETTINGS_GAMES_TOGGLE_OVERLAY.+?}}\)/,
|
||||||
replace: (_, platformCheck, restWithoutPlatformCheck, props, children) => "false"
|
replace: "$&,$self.renderToggleGameActivityButton($<props>)"
|
||||||
+ `${restWithoutPlatformCheck}`
|
|
||||||
+ `(${platformCheck}?${children}:[])`
|
|
||||||
+ `.concat(Vencord.Plugins.plugins.IgnoreActivities.renderToggleGameActivityButton(${props}))`
|
|
||||||
}
|
}
|
||||||
},
|
}, {
|
||||||
{
|
|
||||||
find: ".overlayBadge",
|
find: ".overlayBadge",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<=\(\)\.badgeContainer.+?(\i)\.name}\):null)/,
|
match: /.badgeContainer.+?.\?\(0,.\.jsx\)\(.{1,2},{name:(?<props>.)\.name}\):null/,
|
||||||
replace: (_, props) => `,$self.renderToggleActivityButton(${props})`
|
replace: "$&,$self.renderToggleActivityButton($<props>)"
|
||||||
}
|
}
|
||||||
},
|
}, {
|
||||||
{
|
|
||||||
find: '.displayName="LocalActivityStore"',
|
find: '.displayName="LocalActivityStore"',
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /LISTENING.+?\)\);(?<=(\i)\.push.+?)/,
|
match: /(?<activities>.)\.push\(.\({type:.\..{1,3}\.LISTENING.+?\)\)/,
|
||||||
replace: (m, activities) => `${m}${activities}=${activities}.filter($self.isActivityNotIgnored);`
|
replace: "$&;$<activities>=$<activities>.filter($self.isActivityNotIgnored);"
|
||||||
}
|
}
|
||||||
}
|
}],
|
||||||
],
|
|
||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
const ignoredActivitiesData = await DataStore.get<string[] | Map<IgnoredActivity["id"], IgnoredActivity>>("IgnoreActivities_ignoredActivities") ?? new Map<IgnoredActivity["id"], IgnoredActivity>();
|
const ignoredActivitiesData = await DataStore.get<string[] | Map<IgnoredActivity["id"], IgnoredActivity>>("IgnoreActivities_ignoredActivities") ?? new Map<IgnoredActivity["id"], IgnoredActivity>();
|
||||||
@ -201,7 +192,7 @@ export default definePlugin({
|
|||||||
renderToggleGameActivityButton(props: { id?: string; exePath: string; }) {
|
renderToggleGameActivityButton(props: { id?: string; exePath: string; }) {
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary noop>
|
<ErrorBoundary noop>
|
||||||
<ToggleActivityComponent activity={{ id: props.id ?? props.exePath, type: ActivitiesTypes.Game }} forceLeftMargin={true} />
|
<ToggleActivityComponent activity={{ id: props.id ?? props.exePath, type: ActivitiesTypes.Game }} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -223,5 +214,5 @@ export default definePlugin({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
@ -1,198 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2023 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { FluxDispatcher, React, useRef, useState } from "@webpack/common";
|
|
||||||
|
|
||||||
import { ELEMENT_ID } from "../constants";
|
|
||||||
import { settings } from "../index";
|
|
||||||
import { waitFor } from "../utils/waitFor";
|
|
||||||
|
|
||||||
interface Vec2 {
|
|
||||||
x: number,
|
|
||||||
y: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MagnifierProps {
|
|
||||||
zoom: number;
|
|
||||||
size: number,
|
|
||||||
instance: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Magnifier: React.FC<MagnifierProps> = ({ instance, size: initialSize, zoom: initalZoom }) => {
|
|
||||||
const [ready, setReady] = useState(false);
|
|
||||||
|
|
||||||
const [lensPosition, setLensPosition] = useState<Vec2>({ x: 0, y: 0 });
|
|
||||||
const [imagePosition, setImagePosition] = useState<Vec2>({ x: 0, y: 0 });
|
|
||||||
const [opacity, setOpacity] = useState(0);
|
|
||||||
|
|
||||||
const isShiftDown = useRef(false);
|
|
||||||
|
|
||||||
const zoom = useRef(initalZoom);
|
|
||||||
const size = useRef(initialSize);
|
|
||||||
|
|
||||||
const element = useRef<HTMLDivElement | null>(null);
|
|
||||||
const currentVideoElementRef = useRef<HTMLVideoElement | null>(null);
|
|
||||||
const originalVideoElementRef = useRef<HTMLVideoElement | null>(null);
|
|
||||||
const imageRef = useRef<HTMLImageElement | null>(null);
|
|
||||||
|
|
||||||
// since we accessing document im gonna use useLayoutEffect
|
|
||||||
React.useLayoutEffect(() => {
|
|
||||||
const onKeyDown = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === "Shift") {
|
|
||||||
isShiftDown.current = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const onKeyUp = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === "Shift") {
|
|
||||||
isShiftDown.current = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const syncVideos = () => {
|
|
||||||
currentVideoElementRef.current!.currentTime = originalVideoElementRef.current!.currentTime;
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateMousePosition = (e: MouseEvent) => {
|
|
||||||
if (instance.state.mouseOver && instance.state.mouseDown) {
|
|
||||||
const offset = size.current / 2;
|
|
||||||
const pos = { x: e.pageX, y: e.pageY };
|
|
||||||
const x = -((pos.x - element.current!.getBoundingClientRect().left) * zoom.current - offset);
|
|
||||||
const y = -((pos.y - element.current!.getBoundingClientRect().top) * zoom.current - offset);
|
|
||||||
setLensPosition({ x: e.x - offset, y: e.y - offset });
|
|
||||||
setImagePosition({ x, y });
|
|
||||||
setOpacity(1);
|
|
||||||
} else {
|
|
||||||
setOpacity(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
const onMouseDown = (e: MouseEvent) => {
|
|
||||||
if (instance.state.mouseOver && e.button === 0 /* left click */) {
|
|
||||||
zoom.current = settings.store.zoom;
|
|
||||||
size.current = settings.store.size;
|
|
||||||
|
|
||||||
// close context menu if open
|
|
||||||
if (document.getElementById("image-context")) {
|
|
||||||
FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" });
|
|
||||||
}
|
|
||||||
|
|
||||||
updateMousePosition(e);
|
|
||||||
setOpacity(1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onMouseUp = () => {
|
|
||||||
setOpacity(0);
|
|
||||||
if (settings.store.saveZoomValues) {
|
|
||||||
settings.store.zoom = zoom.current;
|
|
||||||
settings.store.size = size.current;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onWheel = async (e: WheelEvent) => {
|
|
||||||
if (instance.state.mouseOver && instance.state.mouseDown && !isShiftDown.current) {
|
|
||||||
const val = zoom.current + ((e.deltaY / 100) * (settings.store.invertScroll ? -1 : 1)) * settings.store.zoomSpeed;
|
|
||||||
zoom.current = val <= 1 ? 1 : val;
|
|
||||||
updateMousePosition(e);
|
|
||||||
}
|
|
||||||
if (instance.state.mouseOver && instance.state.mouseDown && isShiftDown.current) {
|
|
||||||
const val = size.current + (e.deltaY * (settings.store.invertScroll ? -1 : 1)) * settings.store.zoomSpeed;
|
|
||||||
size.current = val <= 50 ? 50 : val;
|
|
||||||
updateMousePosition(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
waitFor(() => instance.state.readyState === "READY", () => {
|
|
||||||
const elem = document.getElementById(ELEMENT_ID) as HTMLDivElement;
|
|
||||||
element.current = elem;
|
|
||||||
elem.firstElementChild!.setAttribute("draggable", "false");
|
|
||||||
if (instance.props.animated) {
|
|
||||||
originalVideoElementRef.current = elem!.querySelector("video")!;
|
|
||||||
originalVideoElementRef.current.addEventListener("timeupdate", syncVideos);
|
|
||||||
setReady(true);
|
|
||||||
} else {
|
|
||||||
setReady(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.addEventListener("keydown", onKeyDown);
|
|
||||||
document.addEventListener("keyup", onKeyUp);
|
|
||||||
document.addEventListener("mousemove", updateMousePosition);
|
|
||||||
document.addEventListener("mousedown", onMouseDown);
|
|
||||||
document.addEventListener("mouseup", onMouseUp);
|
|
||||||
document.addEventListener("wheel", onWheel);
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener("keydown", onKeyDown);
|
|
||||||
document.removeEventListener("keyup", onKeyUp);
|
|
||||||
document.removeEventListener("mousemove", updateMousePosition);
|
|
||||||
document.removeEventListener("mousedown", onMouseDown);
|
|
||||||
document.removeEventListener("mouseup", onMouseUp);
|
|
||||||
document.removeEventListener("wheel", onWheel);
|
|
||||||
|
|
||||||
if (settings.store.saveZoomValues) {
|
|
||||||
settings.store.zoom = zoom.current;
|
|
||||||
settings.store.size = size.current;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!ready) return null;
|
|
||||||
|
|
||||||
const box = element.current!.getBoundingClientRect();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="vc-imgzoom-lens"
|
|
||||||
style={{
|
|
||||||
opacity,
|
|
||||||
width: size.current + "px",
|
|
||||||
height: size.current + "px",
|
|
||||||
transform: `translate(${lensPosition.x}px, ${lensPosition.y}px)`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{instance.props.animated ?
|
|
||||||
(
|
|
||||||
<video
|
|
||||||
ref={currentVideoElementRef}
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
left: `${imagePosition.x}px`,
|
|
||||||
top: `${imagePosition.y}px`
|
|
||||||
}}
|
|
||||||
width={`${box.width * zoom.current}px`}
|
|
||||||
height={`${box.height * zoom.current}px`}
|
|
||||||
poster={instance.props.src}
|
|
||||||
src={originalVideoElementRef.current?.src ?? instance.props.src}
|
|
||||||
autoPlay
|
|
||||||
loop
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<img
|
|
||||||
ref={imageRef}
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
transform: `translate(${imagePosition.x}px, ${imagePosition.y}px)`
|
|
||||||
}}
|
|
||||||
width={`${box.width * zoom.current}px`}
|
|
||||||
height={`${box.height * zoom.current}px`}
|
|
||||||
src={instance.props.src}
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,19 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2023 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const ELEMENT_ID = "vc-imgzoom-magnify-modal";
|
|
@ -1,234 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2023 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
|
|
||||||
import { definePluginSettings } from "@api/settings";
|
|
||||||
import { disableStyle, enableStyle } from "@api/Styles";
|
|
||||||
import { makeRange } from "@components/PluginSettings/components";
|
|
||||||
import { Devs } from "@utils/constants";
|
|
||||||
import { debounce } from "@utils/debounce";
|
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
|
||||||
import { Menu, React, ReactDOM } from "@webpack/common";
|
|
||||||
import type { Root } from "react-dom/client";
|
|
||||||
|
|
||||||
import { Magnifier, MagnifierProps } from "./components/Magnifier";
|
|
||||||
import { ELEMENT_ID } from "./constants";
|
|
||||||
import styles from "./styles.css?managed";
|
|
||||||
|
|
||||||
export const settings = definePluginSettings({
|
|
||||||
saveZoomValues: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
description: "Whether to save zoom and lens size values",
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
preventCarouselFromClosingOnClick: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
// Thanks chat gpt
|
|
||||||
description: "Allow the image modal in the image slideshow thing / carousel to remain open when clicking on the image",
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
invertScroll: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
description: "Invert scroll",
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
zoom: {
|
|
||||||
description: "Zoom of the lens",
|
|
||||||
type: OptionType.SLIDER,
|
|
||||||
markers: makeRange(1, 50, 4),
|
|
||||||
default: 2,
|
|
||||||
stickToMarkers: false,
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
description: "Radius / Size of the lens",
|
|
||||||
type: OptionType.SLIDER,
|
|
||||||
markers: makeRange(50, 1000, 50),
|
|
||||||
default: 100,
|
|
||||||
stickToMarkers: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
zoomSpeed: {
|
|
||||||
description: "How fast the zoom / lens size changes",
|
|
||||||
type: OptionType.SLIDER,
|
|
||||||
markers: makeRange(0.1, 5, 0.2),
|
|
||||||
default: 0.5,
|
|
||||||
stickToMarkers: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const imageContextMenuPatch: NavContextMenuPatchCallback = children => () => {
|
|
||||||
children.push(
|
|
||||||
<Menu.MenuGroup id="image-zoom">
|
|
||||||
{/* thanks SpotifyControls */}
|
|
||||||
<Menu.MenuControlItem
|
|
||||||
id="zoom"
|
|
||||||
label="Zoom"
|
|
||||||
control={(props, ref) => (
|
|
||||||
<Menu.MenuSliderControl
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
minValue={1}
|
|
||||||
maxValue={50}
|
|
||||||
value={settings.store.zoom}
|
|
||||||
onChange={debounce((value: number) => settings.store.zoom = value, 100)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Menu.MenuControlItem
|
|
||||||
id="size"
|
|
||||||
label="Lens Size"
|
|
||||||
control={(props, ref) => (
|
|
||||||
<Menu.MenuSliderControl
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
minValue={50}
|
|
||||||
maxValue={1000}
|
|
||||||
value={settings.store.size}
|
|
||||||
onChange={debounce((value: number) => settings.store.size = value, 100)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Menu.MenuControlItem
|
|
||||||
id="zoom-speed"
|
|
||||||
label="Zoom Speed"
|
|
||||||
control={(props, ref) => (
|
|
||||||
<Menu.MenuSliderControl
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
minValue={0.1}
|
|
||||||
maxValue={5}
|
|
||||||
value={settings.store.zoomSpeed}
|
|
||||||
onChange={debounce((value: number) => settings.store.zoomSpeed = value, 100)}
|
|
||||||
renderValue={(value: number) => `${value.toFixed(3)}x`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Menu.MenuGroup>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "ImageZoom",
|
|
||||||
description: "Lets you zoom in to images and gifs. Use scroll wheel to zoom in and shift + scroll wheel to increase lens radius / size",
|
|
||||||
authors: [Devs.Aria],
|
|
||||||
patches: [
|
|
||||||
{
|
|
||||||
find: '"renderLinkComponent","maxWidth"',
|
|
||||||
replacement: {
|
|
||||||
match: /(return\(.{1,100}\(\)\.wrapper.{1,100})(src)/,
|
|
||||||
replace: `$1id: '${ELEMENT_ID}',$2`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
find: "handleImageLoad=",
|
|
||||||
replacement: [
|
|
||||||
{
|
|
||||||
match: /(render=function\(\){.{1,500}limitResponsiveWidth.{1,600})onMouseEnter:/,
|
|
||||||
replace: "$1...$self.makeProps(this),onMouseEnter:"
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
match: /componentDidMount=function\(\){/,
|
|
||||||
replace: "$&$self.renderMagnifier(this);",
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
match: /componentWillUnmount=function\(\){/,
|
|
||||||
replace: "$&$self.unMountMagnifier();"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
find: ".carouselModal,",
|
|
||||||
replacement: {
|
|
||||||
match: /onClick:(\i),/,
|
|
||||||
replace: "onClick:$self.settings.store.preventCarouselFromClosingOnClick ? () => {} : $1,"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
settings,
|
|
||||||
|
|
||||||
// to stop from rendering twice /shrug
|
|
||||||
currentMagnifierElement: null as React.FunctionComponentElement<MagnifierProps & JSX.IntrinsicAttributes> | null,
|
|
||||||
element: null as HTMLDivElement | null,
|
|
||||||
|
|
||||||
Magnifier,
|
|
||||||
root: null as Root | null,
|
|
||||||
makeProps(instance) {
|
|
||||||
return {
|
|
||||||
onMouseOver: () => this.onMouseOver(instance),
|
|
||||||
onMouseOut: () => this.onMouseOut(instance),
|
|
||||||
onMouseDown: (e: React.MouseEvent) => this.onMouseDown(e, instance),
|
|
||||||
onMouseUp: () => this.onMouseUp(instance),
|
|
||||||
id: instance.props.id,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
renderMagnifier(instance) {
|
|
||||||
if (instance.props.id === ELEMENT_ID) {
|
|
||||||
if (!this.currentMagnifierElement) {
|
|
||||||
this.currentMagnifierElement = <Magnifier size={settings.store.size} zoom={settings.store.zoom} instance={instance} />;
|
|
||||||
this.root = ReactDOM.createRoot(this.element!);
|
|
||||||
this.root.render(this.currentMagnifierElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
unMountMagnifier() {
|
|
||||||
this.root?.unmount();
|
|
||||||
this.currentMagnifierElement = null;
|
|
||||||
this.root = null;
|
|
||||||
},
|
|
||||||
|
|
||||||
onMouseOver(instance) {
|
|
||||||
instance.setState((state: any) => ({ ...state, mouseOver: true }));
|
|
||||||
},
|
|
||||||
onMouseOut(instance) {
|
|
||||||
instance.setState((state: any) => ({ ...state, mouseOver: false }));
|
|
||||||
},
|
|
||||||
onMouseDown(e: React.MouseEvent, instance) {
|
|
||||||
if (e.button === 0 /* left */)
|
|
||||||
instance.setState((state: any) => ({ ...state, mouseDown: true }));
|
|
||||||
},
|
|
||||||
onMouseUp(instance) {
|
|
||||||
instance.setState((state: any) => ({ ...state, mouseDown: false }));
|
|
||||||
},
|
|
||||||
|
|
||||||
start() {
|
|
||||||
enableStyle(styles);
|
|
||||||
addContextMenuPatch("image-context", imageContextMenuPatch);
|
|
||||||
this.element = document.createElement("div");
|
|
||||||
this.element.classList.add("MagnifierContainer");
|
|
||||||
document.body.appendChild(this.element);
|
|
||||||
},
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
disableStyle(styles);
|
|
||||||
// so componenetWillUnMount gets called if Magnifier component is still alive
|
|
||||||
this.root && this.root.unmount();
|
|
||||||
this.element?.remove();
|
|
||||||
removeContextMenuPatch("image-context", imageContextMenuPatch);
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,31 +0,0 @@
|
|||||||
.vc-imgzoom-lens {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 9999;
|
|
||||||
border: 2px solid grey;
|
|
||||||
border-radius: 50%;
|
|
||||||
overflow: hidden;
|
|
||||||
cursor: none;
|
|
||||||
box-shadow: inset 0 0 10px 2px grey;
|
|
||||||
filter: drop-shadow(0 0 2px grey);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* make the carousel take up less space so we can click the backdrop and exit out of it */
|
|
||||||
[class|="carouselModal"] {
|
|
||||||
height: fit-content;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
[class*="modalCarouselWrapper"] {
|
|
||||||
height: fit-content;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
[class|="wrapper"]:has(> #vc-imgzoom-magnify-modal) {
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
top: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user