Compare commits
39 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
4aa7a052d0 | ||
|
f088f17a0a | ||
|
a55c758b0e | ||
|
f092f434fe | ||
|
2e6dfaa879 | ||
|
96dc2e12d0 | ||
|
d931790ed0 | ||
|
6b26c12bfa | ||
|
5bb08bdb64 | ||
|
405be7ef13 | ||
|
a7e2fb48ba | ||
|
ae80749dd8 | ||
|
8c47b7080d | ||
|
8378638ee4 | ||
|
7c563471f6 | ||
|
29382d2781 | ||
|
6226672ee8 | ||
|
5b5ee82f27 | ||
|
62f74f5917 | ||
|
265c7a18a7 | ||
|
462f191051 | ||
|
6960a439c9 | ||
|
4dff1c5bd5 | ||
|
2c8ebdce7d | ||
|
dae7cb67ef | ||
|
081b01b667 | ||
|
5340ea7ba0 | ||
|
84a649a671 | ||
|
efd9927696 | ||
|
c86a34a15d | ||
|
ff16513f21 | ||
|
906c265aea | ||
|
708c16176b | ||
|
035d1e24b2 | ||
|
48e9b1be7a | ||
|
6acdaf207d | ||
|
9d41b360c9 | ||
|
12cbd73e7f | ||
|
420b068094 |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@ -42,7 +42,7 @@ jobs:
|
||||
|
||||
- name: Clean up obsolete files
|
||||
run: |
|
||||
rm -rf dist/extension* Vencord.user.css
|
||||
rm -rf dist/extension* Vencord.user.css vencordDesktopRenderer.css vencordDesktopRenderer.css.map
|
||||
|
||||
- name: Get some values needed for the release
|
||||
id: release_values
|
||||
|
2
.github/workflows/reportBrokenPlugins.yml
vendored
2
.github/workflows/reportBrokenPlugins.yml
vendored
@ -50,7 +50,7 @@ jobs:
|
||||
export CHROMIUM_BIN=$(which chromium-browser)
|
||||
export USE_CANARY=true
|
||||
|
||||
esbuild test/generateReport.ts > dist/report.mjs
|
||||
esbuild scripts/generateReport.ts > dist/report.mjs
|
||||
node dist/report.mjs >> $GITHUB_STEP_SUMMARY
|
||||
env:
|
||||
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
|
||||
|
@ -6,7 +6,7 @@ The cutest Discord client mod
|
||||
|
||||
- Super easy to install (Download Installer, open, click install button, done)
|
||||
- 100+ plugins built in: [See a list](https://gist.github.com/Vendicated/8696cde7b92548064a3ae92ead84d033)
|
||||
- Some highlights: SpotifyControls, Experiments, NoTrack, MessageLogger, QuickReply, Free Emotes/Stickers, CustomCommands, ShowHiddenChannels, PronounDB
|
||||
- Some highlights: SpotifyControls, GameActivityToggle, 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
|
||||
- Works on any Discord branch: Stable, Canary or PTB all work (though for the best experience I recommend stable!)
|
||||
|
BIN
browser/icon.png
BIN
browser/icon.png
Binary file not shown.
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 1.1 KiB |
@ -13,12 +13,6 @@ Welcome to Megu's Installation Guide! In this file, you will learn about how to
|
||||
- [Installing Vencord](#installing-vencord)
|
||||
- [Updating Vencord](#updating-vencord)
|
||||
- [Uninstalling Vencord](#uninstalling-vencord)
|
||||
- [Manually Installing Vencord](#manually-installing-vencord)
|
||||
- [On Windows](#on-windows)
|
||||
- [On Linux](#on-linux)
|
||||
- [On MacOS](#on-macos)
|
||||
- [Manual Patching](#manual-patching)
|
||||
- [Manually Uninstalling Vencord](#manually-uninstalling-vencord)
|
||||
|
||||
## Dependencies
|
||||
|
||||
@ -27,11 +21,9 @@ Welcome to Megu's Installation Guide! In this file, you will learn about how to
|
||||
|
||||
## Installing Vencord
|
||||
|
||||
> :exclamation: If this doesn't work, see [Manually Installing Vencord](#manually-installing-vencord)
|
||||
|
||||
Install `pnpm`:
|
||||
|
||||
> :exclamation: This next command may need to be run as admin/sudo depending on your system, and you may need to close and reopen your terminal for pnpm to be in your PATH.
|
||||
> :exclamation: This next command may need to be run as admin/root depending on your system, and you may need to close and reopen your terminal for pnpm to be in your PATH.
|
||||
|
||||
```shell
|
||||
npm i -g pnpm
|
||||
@ -103,102 +95,4 @@ Simply run:
|
||||
pnpm uninject
|
||||
```
|
||||
|
||||
The above command may ask you to also run:
|
||||
|
||||
```shell
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm uninject
|
||||
```
|
||||
|
||||
## Manually Installing Vencord
|
||||
|
||||
- [Windows](#on-windows)
|
||||
- [Linux](#on-linux)
|
||||
- [MacOS](#on-macos)
|
||||
|
||||
### On Windows
|
||||
|
||||
Press Win+R and enter: `%LocalAppData%` and hit enter. In this page, find the page (Discord, DiscordPTB, DiscordCanary, etc) that you want to patch.
|
||||
|
||||
Now follow the instructions at [Manual Patching](#manual-patching)
|
||||
|
||||
### On Linux
|
||||
|
||||
The Discord folder is usually in one of the following paths:
|
||||
|
||||
- /usr/share
|
||||
- /usr/lib64
|
||||
- /opt
|
||||
- /home/$USER/.local/share
|
||||
|
||||
If you use flatpak, it will usually be in one of the following paths:
|
||||
|
||||
- /var/lib/flatpak/app/com.discordapp.Discord/current/active/files
|
||||
- /home/$USER/.local/share/flatpak/app/com.discordapp.Discord/current/active/files
|
||||
|
||||
You will need to give flatpak access to vencord with one of the following commands:
|
||||
|
||||
> :exclamation: If not on stable, replace `com.discordapp.Discord` with your branch name, e.g., `com.discordapp.DiscordCanary`
|
||||
|
||||
> :exclamation: Replace `/path/to/vencord/` with the path to your vencord folder (NOT the dist folder)
|
||||
|
||||
If Discord flatpak install is in /home/:
|
||||
|
||||
```shell
|
||||
flatpak override --user com.discordapp.Discord --filesystem="/path/to/vencord/"
|
||||
```
|
||||
|
||||
If Discord flatpak install not in /home/:
|
||||
|
||||
```shell
|
||||
sudo flatpak override com.discordapp.Discord --filesystem="/path/to/vencord"
|
||||
```
|
||||
|
||||
Now follow the instructions at [Manual Patching](#manual-patching)
|
||||
|
||||
### On MacOS
|
||||
|
||||
Open finder and go to your Applications folder. Right-Click on the Discord application you want to patch, and view contents.
|
||||
|
||||
Go to the `Contents/Resources` folder.
|
||||
|
||||
Now follow the instructions at [Manual Patching](#manual-patching)
|
||||
|
||||
### Manual Patching
|
||||
|
||||
> :exclamation: If using Flatpak on linux, go to the folder that contains the `app.asar` file, and skip to where we create the `app` folder below.
|
||||
|
||||
> :exclamation: On Linux/MacOS, there's a chance there won't be an `app-<number>` folder, but there probably is a `resources` folder, so keep reading :)
|
||||
|
||||
Inside there, look for the `app-<number>` folders. If you have multiple, use the highest number. If that doesn't work, do it for the rest of the `app-<number>` folders.
|
||||
|
||||
Inside there, go to the `resources` folder. There should be a file called `app.asar`. If there isn't, look at a different `app-<number>` folder instead.
|
||||
|
||||
Make a new folder in `resources` called `app`. In here, we will make two files:
|
||||
|
||||
`package.json` and `index.js`
|
||||
|
||||
In `index.js`:
|
||||
|
||||
> :exclamation: Replace the path in the first line with the path to `patcher.js` in your vencord dist folder.
|
||||
> On Windows, you can get this by shift-rightclicking the patcher.js file and selecting "copy as path"
|
||||
|
||||
```js
|
||||
require("C:/Users/<your user>/path/to/vencord/dist/patcher.js");
|
||||
```
|
||||
|
||||
And in `package.json`:
|
||||
|
||||
```json
|
||||
{ "name": "discord", "main": "index.js" }
|
||||
```
|
||||
|
||||
Finally, fully close & reopen your Discord client and check to see that `Vencord` appears in settings!
|
||||
|
||||
### Manually Uninstalling Vencord
|
||||
|
||||
> :exclamation: Do not delete `app.asar` - Only delete the `app` folder we created.
|
||||
|
||||
Use the instructions above to find the `app` folder, and delete it. Then Close & Reopen Discord.
|
||||
|
||||
If you need more help, ask in the support channel in our [Discord Server](https://discord.gg/D9uwnFnqmd).
|
||||
|
@ -1,9 +1,8 @@
|
||||
{
|
||||
"name": "vencord",
|
||||
"private": "true",
|
||||
"version": "1.1.3",
|
||||
"version": "1.1.5",
|
||||
"description": "The cutest Discord client mod",
|
||||
"keywords": [ ],
|
||||
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/Vendicated/Vencord/issues"
|
||||
@ -34,7 +33,9 @@
|
||||
"dependencies": {
|
||||
"@vap/core": "0.0.12",
|
||||
"@vap/shiki": "0.10.3",
|
||||
"fflate": "^0.7.4"
|
||||
"fflate": "^0.7.4",
|
||||
"nanoid": "^4.0.2",
|
||||
"virtual-merge": "^1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/diff": "^5.0.2",
|
||||
|
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
@ -31,6 +31,7 @@ specifiers:
|
||||
fflate: ^0.7.4
|
||||
highlight.js: 10.6.0
|
||||
moment: ^2.29.4
|
||||
nanoid: ^4.0.2
|
||||
puppeteer-core: ^19.6.0
|
||||
standalone-electron-types: ^1.0.0
|
||||
stylelint: ^14.16.1
|
||||
@ -38,11 +39,14 @@ specifiers:
|
||||
tsx: ^3.12.6
|
||||
type-fest: ^3.5.3
|
||||
typescript: ^4.9.4
|
||||
virtual-merge: ^1.0.1
|
||||
|
||||
dependencies:
|
||||
'@vap/core': 0.0.12
|
||||
'@vap/shiki': 0.10.3
|
||||
fflate: 0.7.4
|
||||
nanoid: 4.0.2
|
||||
virtual-merge: 1.0.1
|
||||
|
||||
devDependencies:
|
||||
'@types/diff': 5.0.2
|
||||
@ -2245,6 +2249,12 @@ packages:
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/nanoid/4.0.2:
|
||||
resolution: {integrity: sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==}
|
||||
engines: {node: ^14 || ^16 || >=18}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/nanomatch/1.2.13:
|
||||
resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -3123,6 +3133,10 @@ packages:
|
||||
spdx-expression-parse: 3.0.1
|
||||
dev: true
|
||||
|
||||
/virtual-merge/1.0.1:
|
||||
resolution: {integrity: sha512-h7rzV6n5fZJbDu2lP4iu+IOtsZ00uqECFUxFePK1uY0pz/S5B7FNDJpmdDVfyGL7poyJECEHfTaIpJaknNkU0Q==}
|
||||
dev: false
|
||||
|
||||
/vscode-oniguruma/1.7.0:
|
||||
resolution: {integrity: sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==}
|
||||
dev: false
|
||||
|
@ -48,6 +48,7 @@ const sourceMapFooter = s => watch ? "" : `//# sourceMappingURL=vencord://${s}.j
|
||||
const sourcemap = watch ? "inline" : "external";
|
||||
|
||||
await Promise.all([
|
||||
// common preload
|
||||
esbuild.build({
|
||||
...nodeCommonOpts,
|
||||
entryPoints: ["src/preload.ts"],
|
||||
@ -55,12 +56,19 @@ await Promise.all([
|
||||
footer: { js: "//# sourceURL=VencordPreload\n" + sourceMapFooter("preload") },
|
||||
sourcemap,
|
||||
}),
|
||||
|
||||
// Discord Desktop main & renderer
|
||||
esbuild.build({
|
||||
...nodeCommonOpts,
|
||||
entryPoints: ["src/patcher.ts"],
|
||||
entryPoints: ["src/main/index.ts"],
|
||||
outfile: "dist/patcher.js",
|
||||
footer: { js: "//# sourceURL=VencordPatcher\n" + sourceMapFooter("patcher") },
|
||||
sourcemap,
|
||||
define: {
|
||||
...defines,
|
||||
IS_DISCORD_DESKTOP: true,
|
||||
IS_VENCORD_DESKTOP: false
|
||||
}
|
||||
}),
|
||||
esbuild.build({
|
||||
...commonOpts,
|
||||
@ -77,7 +85,43 @@ await Promise.all([
|
||||
],
|
||||
define: {
|
||||
...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,
|
||||
...commonOpts.plugins
|
||||
],
|
||||
define: {
|
||||
...defines,
|
||||
IS_WEB: false,
|
||||
IS_DISCORD_DESKTOP: false,
|
||||
IS_VENCORD_DESKTOP: true
|
||||
}
|
||||
}),
|
||||
]).catch(err => {
|
||||
|
@ -36,7 +36,7 @@ const commonOptions = {
|
||||
entryPoints: ["browser/Vencord.ts"],
|
||||
globalName: "Vencord",
|
||||
format: "iife",
|
||||
external: ["plugins", "git-hash"],
|
||||
external: ["plugins", "git-hash", "/assets/*"],
|
||||
plugins: [
|
||||
globPlugins,
|
||||
...commonOpts.plugins,
|
||||
@ -45,7 +45,9 @@ const commonOptions = {
|
||||
define: {
|
||||
IS_WEB: "true",
|
||||
IS_STANDALONE: "true",
|
||||
IS_DEV: JSON.stringify(watch)
|
||||
IS_DEV: JSON.stringify(watch),
|
||||
IS_DISCORD_DESKTOP: "false",
|
||||
IS_VENCORD_DESKTOP: "false"
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -193,7 +193,7 @@ export const commonOpts = {
|
||||
legalComments: "linked",
|
||||
banner,
|
||||
plugins: [fileIncludePlugin, gitHashPlugin, gitRemotePlugin, stylePlugin],
|
||||
external: ["~plugins", "~git-hash", "~git-remote"],
|
||||
external: ["~plugins", "~git-hash", "~git-remote", "/assets/*"],
|
||||
inject: ["./scripts/build/inject/react.mjs"],
|
||||
jsxFactory: "VencordCreateElement",
|
||||
jsxFragment: "VencordFragment",
|
||||
|
@ -30,7 +30,7 @@ import "./webpack/patchWebpack";
|
||||
import { showNotification } from "./api/Notifications";
|
||||
import { PlainSettings, Settings } from "./api/settings";
|
||||
import { patches, PMLogger, startAllPlugins } from "./plugins";
|
||||
import { checkForUpdates, rebuild, update, UpdateLogger } from "./utils/updater";
|
||||
import { checkForUpdates, rebuild, update,UpdateLogger } from "./utils/updater";
|
||||
import { onceReady } from "./webpack";
|
||||
import { SettingsRouter } from "./webpack/common";
|
||||
|
||||
@ -54,9 +54,14 @@ async function init() {
|
||||
title: "Vencord has been updated!",
|
||||
body: "Click here to restart",
|
||||
permanent: true,
|
||||
noPersist: true,
|
||||
onClick() {
|
||||
if (needsFullRestart)
|
||||
window.DiscordNative.app.relaunch();
|
||||
if (needsFullRestart) {
|
||||
if (IS_DISCORD_DESKTOP)
|
||||
window.DiscordNative.app.relaunch();
|
||||
else
|
||||
window.VencordDesktop.app.relaunch();
|
||||
}
|
||||
else
|
||||
location.reload();
|
||||
}
|
||||
@ -69,6 +74,7 @@ async function init() {
|
||||
title: "A Vencord update is available!",
|
||||
body: "Click here to view the update",
|
||||
permanent: true,
|
||||
noPersist: true,
|
||||
onClick() {
|
||||
SettingsRouter.open("VencordUpdater");
|
||||
}
|
||||
@ -94,7 +100,7 @@ async function init() {
|
||||
|
||||
init();
|
||||
|
||||
if (!IS_WEB && Settings.winNativeTitleBar && navigator.platform.toLowerCase().startsWith("win")) {
|
||||
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",
|
||||
|
@ -20,6 +20,7 @@ import "./styles.css";
|
||||
|
||||
import { useSettings } from "@api/settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { classes } from "@utils/misc";
|
||||
import { React, useEffect, useMemo, useState, useStateFromStores, WindowStore } from "@webpack/common";
|
||||
|
||||
import { NotificationData } from "./Notifications";
|
||||
@ -33,8 +34,10 @@ export default ErrorBoundary.wrap(function NotificationComponent({
|
||||
onClick,
|
||||
onClose,
|
||||
image,
|
||||
permanent
|
||||
}: NotificationData) {
|
||||
permanent,
|
||||
className,
|
||||
dismissOnClick
|
||||
}: NotificationData & { className?: string; }) {
|
||||
const { timeout, position } = useSettings(["notifications.timeout", "notifications.position"]).notifications;
|
||||
const hasFocus = useStateFromStores([WindowStore], () => WindowStore.isFocused());
|
||||
|
||||
@ -61,11 +64,12 @@ export default ErrorBoundary.wrap(function NotificationComponent({
|
||||
|
||||
return (
|
||||
<button
|
||||
className="vc-notification-root"
|
||||
className={classes("vc-notification-root", className)}
|
||||
style={position === "bottom-right" ? { bottom: "1rem" } : { top: "3rem" }}
|
||||
onClick={() => {
|
||||
onClose!();
|
||||
onClick?.();
|
||||
if (dismissOnClick !== false)
|
||||
onClose!();
|
||||
}}
|
||||
onContextMenu={e => {
|
||||
e.preventDefault();
|
||||
|
@ -23,6 +23,7 @@ import type { ReactNode } from "react";
|
||||
import type { Root } from "react-dom/client";
|
||||
|
||||
import NotificationComponent from "./NotificationComponent";
|
||||
import { persistNotification } from "./notificationLog";
|
||||
|
||||
const NotificationQueue = new Queue();
|
||||
|
||||
@ -56,6 +57,10 @@ export interface NotificationData {
|
||||
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) {
|
||||
@ -86,6 +91,8 @@ export async function requestPermission() {
|
||||
}
|
||||
|
||||
export async function showNotification(data: NotificationData) {
|
||||
persistNotification(data);
|
||||
|
||||
if (shouldBeNative() && await requestPermission()) {
|
||||
const { title, body, icon, image, onClick = null, onClose = null } = data;
|
||||
const n = new Notification(title, {
|
||||
|
203
src/api/Notifications/notificationLog.tsx
Normal file
203
src/api/Notifications/notificationLog.tsx
Normal file
@ -0,0 +1,203 @@
|
||||
/*
|
||||
* 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,16 +3,20 @@
|
||||
all: unset;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 25vw;
|
||||
min-height: 10vh;
|
||||
color: var(--text-normal);
|
||||
background-color: var(--background-secondary-alt);
|
||||
position: absolute;
|
||||
z-index: 2147483647;
|
||||
right: 1rem;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.vc-notification-root:not(.vc-notification-log-wrapper > .vc-notification-root) {
|
||||
position: absolute;
|
||||
z-index: 2147483647;
|
||||
right: 1rem;
|
||||
width: 25vw;
|
||||
min-height: 10vh;
|
||||
}
|
||||
|
||||
.vc-notification {
|
||||
@ -72,3 +76,47 @@
|
||||
.vc-notification-img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.vc-notification-log-empty {
|
||||
height: 218px;
|
||||
background: url("/assets/b36de980b174d7b798c89f35c116e5c6.svg") center no-repeat;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.vc-notification-log-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1em;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.vc-notification-log-wrapper {
|
||||
transition: 200ms ease;
|
||||
transition-property: height, opacity;
|
||||
}
|
||||
|
||||
.vc-notification-log-wrapper:not(:last-child) {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.vc-notification-log-removing {
|
||||
height: 0 !important;
|
||||
opacity: 0;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.vc-notification-log-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.vc-notification-log-timestamp {
|
||||
margin-left: auto;
|
||||
font-size: 0.8em;
|
||||
font-weight: lighter;
|
||||
}
|
||||
|
||||
.vc-notification-log-danger-btn {
|
||||
color: var(--white-500);
|
||||
background-color: var(--button-danger-background);
|
||||
}
|
||||
|
69
src/api/SettingsStore.ts
Normal file
69
src/api/SettingsStore.ts
Normal file
@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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));
|
||||
}
|
@ -28,6 +28,7 @@ import * as $MessagePopover from "./MessagePopover";
|
||||
import * as $Notices from "./Notices";
|
||||
import * as $Notifications from "./Notifications";
|
||||
import * as $ServerList from "./ServerList";
|
||||
import * as $SettingsStore from "./SettingsStore";
|
||||
import * as $Styles from "./Styles";
|
||||
|
||||
/**
|
||||
@ -85,6 +86,10 @@ export const MessageDecorations = $MessageDecorations;
|
||||
* An API allowing you to add components to member list users, in both DM's and servers
|
||||
*/
|
||||
export const MemberListDecorators = $MemberListDecorators;
|
||||
/**
|
||||
* An API allowing you to read, manipulate and automatically update components based on Discord settings
|
||||
*/
|
||||
export const SettingsStore = $SettingsStore;
|
||||
/**
|
||||
* An API allowing you to dynamically load styles
|
||||
* a
|
||||
|
@ -47,6 +47,7 @@ export interface Settings {
|
||||
timeout: number;
|
||||
position: "top-right" | "bottom-right";
|
||||
useNative: "always" | "never" | "not-focused";
|
||||
logLimit: number;
|
||||
};
|
||||
}
|
||||
|
||||
@ -66,7 +67,8 @@ const DefaultSettings: Settings = {
|
||||
notifications: {
|
||||
timeout: 5000,
|
||||
position: "bottom-right",
|
||||
useNative: "not-focused"
|
||||
useNative: "not-focused",
|
||||
logLimit: 50
|
||||
}
|
||||
};
|
||||
|
||||
@ -133,6 +135,7 @@ function makeProxy(settings: any, root = settings, path = ""): Settings {
|
||||
target[p] = v;
|
||||
// Call any listeners that are listening to a setting of this path
|
||||
const setPath = `${path}${path && "."}${p}`;
|
||||
delete proxyCache[setPath];
|
||||
for (const subscription of subscriptions) {
|
||||
if (!subscription._path || subscription._path === setPath) {
|
||||
subscription(v, setPath);
|
||||
|
@ -46,6 +46,7 @@ const cl = classNameFactory("vc-plugins-");
|
||||
const logger = new Logger("PluginSettings", "#a6d189");
|
||||
|
||||
const InputStyles = findByPropsLazy("inputDefault", "inputWrapper");
|
||||
const ButtonClasses = findByPropsLazy("button", "disabled", "enabled");
|
||||
|
||||
const CogWheel = LazyComponent(() => findByCode("18.564C15.797 19.099 14.932 19.498 14 19.738V22H10V19.738C9.069"));
|
||||
const InfoIcon = LazyComponent(() => findByCode("4.4408921e-16 C4.4771525,-1.77635684e-15 4.4408921e-16"));
|
||||
@ -154,7 +155,7 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe
|
||||
<Text variant="text-md/bold" className={cl("name")}>
|
||||
{plugin.name}{isNew && <Badge text="NEW" color="#ED4245" />}
|
||||
</Text>
|
||||
<button role="switch" onClick={() => openModal()} className={classes("button-12Fmur", cl("info-button"))}>
|
||||
<button role="switch" onClick={() => openModal()} className={classes(ButtonClasses.button, cl("info-button"))}>
|
||||
{plugin.options
|
||||
? <CogWheel />
|
||||
: <InfoIcon width="24" height="24" />}
|
||||
|
@ -24,6 +24,7 @@ import { handleComponentFailed } from "@components/handleComponentFailed";
|
||||
import { Link } from "@components/Link";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes, useAwaiter } from "@utils/misc";
|
||||
import { relaunch } from "@utils/native";
|
||||
import { changes, checkForUpdates, getRepo, isNewer, rebuild, update, updateError, UpdateLogger } from "@utils/updater";
|
||||
import { Alerts, Button, Card, Forms, Parser, React, Switch, Toasts } from "@webpack/common";
|
||||
|
||||
@ -133,7 +134,7 @@ function Updatable(props: CommonProps) {
|
||||
cancelText: "Not now!",
|
||||
onConfirm() {
|
||||
if (needFullRestart)
|
||||
window.DiscordNative.app.relaunch();
|
||||
relaunch();
|
||||
else
|
||||
location.reload();
|
||||
r();
|
||||
|
@ -17,6 +17,7 @@
|
||||
*/
|
||||
|
||||
|
||||
import { openNotificationLogModal } from "@api/Notifications/notificationLog";
|
||||
import { useSettings } from "@api/settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import DonateButton from "@components/DonateButton";
|
||||
@ -25,6 +26,7 @@ import { ErrorCard } from "@components/ErrorCard";
|
||||
import IpcEvents from "@utils/IpcEvents";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { identity, useAwaiter } from "@utils/misc";
|
||||
import { relaunch } from "@utils/native";
|
||||
import { Button, Card, Forms, React, Select, Slider, Switch } from "@webpack/common";
|
||||
|
||||
const cl = classNameFactory("vc-settings-");
|
||||
@ -72,7 +74,7 @@ function VencordSettings() {
|
||||
title: "Use Windows' native title bar instead of Discord's custom one",
|
||||
note: "Requires a full restart"
|
||||
}),
|
||||
!IS_WEB && {
|
||||
!IS_WEB && false /* This causes electron to freeze / white screen for some people */ && {
|
||||
key: "transparent",
|
||||
title: "Enable window transparency",
|
||||
note: "Requires a full restart"
|
||||
@ -99,7 +101,7 @@ function VencordSettings() {
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<Button
|
||||
onClick={() => window.DiscordNative.app.relaunch()}
|
||||
onClick={relaunch}
|
||||
size={Button.Sizes.SMALL}>
|
||||
Restart Client
|
||||
</Button>
|
||||
@ -110,6 +112,7 @@ function VencordSettings() {
|
||||
Open QuickCSS File
|
||||
</Button>
|
||||
<Button
|
||||
// FIXME: Vencord Desktop support
|
||||
onClick={() => window.DiscordNative.fileManager.showItemInFolder(settingsDir)}
|
||||
size={Button.Sizes.SMALL}
|
||||
disabled={settingsDirPending}>
|
||||
@ -165,7 +168,7 @@ function VencordSettings() {
|
||||
{ label: "Only use Desktop notifications when Discord is not focused", value: "not-focused", default: true },
|
||||
{ label: "Always use Desktop notifications", value: "always" },
|
||||
{ label: "Always use Vencord notifications", value: "never" },
|
||||
]satisfies Array<{ value: typeof settings["notifications"]["useNative"]; } & Record<string, any>>}
|
||||
] satisfies Array<{ value: typeof settings["notifications"]["useNative"]; } & Record<string, any>>}
|
||||
closeOnSelect={true}
|
||||
select={v => notifSettings.useNative = v}
|
||||
isSelected={v => v === notifSettings.useNative}
|
||||
@ -179,7 +182,7 @@ function VencordSettings() {
|
||||
options={[
|
||||
{ label: "Bottom Right", value: "bottom-right", default: true },
|
||||
{ label: "Top Right", value: "top-right" },
|
||||
]satisfies Array<{ value: typeof settings["notifications"]["position"]; } & Record<string, any>>}
|
||||
] satisfies Array<{ value: typeof settings["notifications"]["position"]; } & Record<string, any>>}
|
||||
select={v => notifSettings.position = v}
|
||||
isSelected={v => v === notifSettings.position}
|
||||
serialize={identity}
|
||||
@ -198,6 +201,29 @@ function VencordSettings() {
|
||||
onMarkerRender={v => (v / 1000) + "s"}
|
||||
stickToMarkers={false}
|
||||
/>
|
||||
|
||||
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Log Limit</Forms.FormTitle>
|
||||
<Forms.FormText className={Margins.bottom16}>
|
||||
The amount of notifications to save in the log until old ones are removed.
|
||||
Set to <code>0</code> to disable Notification log and <code>∞</code> to never automatically remove old Notifications
|
||||
</Forms.FormText>
|
||||
<Slider
|
||||
markers={[0, 25, 50, 75, 100, 200]}
|
||||
minValue={0}
|
||||
maxValue={200}
|
||||
stickToMarkers={true}
|
||||
initialValue={notifSettings.logLimit}
|
||||
onValueChange={v => notifSettings.logLimit = v}
|
||||
onValueRender={v => v === 200 ? "∞" : v}
|
||||
onMarkerRender={v => v === 200 ? "∞" : v}
|
||||
/>
|
||||
|
||||
<Button
|
||||
onClick={openNotificationLogModal}
|
||||
disabled={notifSettings.logLimit === 0}
|
||||
>
|
||||
Open Notification Log
|
||||
</Button>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
@ -21,8 +21,7 @@ import "./settingsStyles.css";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { handleComponentFailed } from "@components/handleComponentFailed";
|
||||
import { findByCodeLazy } from "@webpack";
|
||||
import { Forms, SettingsRouter, Text } from "@webpack/common";
|
||||
import { Forms, SettingsRouter, TabBar, Text } from "@webpack/common";
|
||||
|
||||
import BackupRestoreTab from "./BackupRestoreTab";
|
||||
import PluginsTab from "./PluginsTab";
|
||||
@ -32,8 +31,6 @@ import VencordSettings from "./VencordTab";
|
||||
|
||||
const cl = classNameFactory("vc-settings-");
|
||||
|
||||
const TabBar = findByCodeLazy('[role="tab"][aria-disabled="false"]');
|
||||
|
||||
interface SettingsProps {
|
||||
tab: string;
|
||||
}
|
||||
|
3
src/globals.d.ts
vendored
3
src/globals.d.ts
vendored
@ -35,6 +35,8 @@ declare global {
|
||||
export var IS_WEB: boolean;
|
||||
export var IS_DEV: boolean;
|
||||
export var IS_STANDALONE: boolean;
|
||||
export var IS_DISCORD_DESKTOP: boolean;
|
||||
export var IS_VENCORD_DESKTOP: boolean;
|
||||
|
||||
export var VencordNative: typeof import("./VencordNative").default;
|
||||
export var Vencord: typeof import("./Vencord");
|
||||
@ -54,6 +56,7 @@ declare global {
|
||||
* If you really must use it, mark your plugin as Desktop App only by naming it Foo.desktop.ts(x)
|
||||
*/
|
||||
export var DiscordNative: any;
|
||||
export var VencordDesktop: any;
|
||||
|
||||
interface Window {
|
||||
webpackChunkdiscord_app: {
|
||||
|
109
src/main/index.ts
Normal file
109
src/main/index.ts
Normal file
@ -0,0 +1,109 @@
|
||||
/*
|
||||
* 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 "preload.js.map":
|
||||
case "patcher.js.map": // doubt
|
||||
case "main.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");
|
||||
}
|
@ -28,7 +28,7 @@ import { join } from "path";
|
||||
|
||||
import monacoHtml from "~fileContent/../components/monacoWin.html;base64";
|
||||
|
||||
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, SETTINGS_FILE } from "./constants";
|
||||
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, SETTINGS_FILE } from "./utils/constants";
|
||||
|
||||
mkdirSync(SETTINGS_DIR, { recursive: true });
|
||||
|
||||
@ -44,6 +44,14 @@ export function readSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
export function getSettings(): typeof import("@api/settings").Settings {
|
||||
try {
|
||||
return JSON.parse(readSettings());
|
||||
} catch {
|
||||
return {} as any;
|
||||
}
|
||||
}
|
||||
|
||||
ipcMain.handle(IpcEvents.OPEN_QUICKCSS, () => shell.openPath(QUICKCSS_PATH));
|
||||
|
||||
ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => {
|
@ -20,9 +20,8 @@ import { onceDefined } from "@utils/onceDefined";
|
||||
import electron, { app, BrowserWindowConstructorOptions, Menu } from "electron";
|
||||
import { dirname, join } from "path";
|
||||
|
||||
import { initIpc } from "./ipcMain";
|
||||
import { installExt } from "./ipcMain/extensions";
|
||||
import { readSettings } from "./ipcMain/index";
|
||||
import { getSettings, initIpc } from "./ipcMain";
|
||||
import { IS_VANILLA } from "./utils/constants";
|
||||
|
||||
console.log("[Vencord] Starting up...");
|
||||
|
||||
@ -41,11 +40,8 @@ require.main!.filename = join(asarPath, discordPkg.main);
|
||||
// @ts-ignore Untyped method? Dies from cringe
|
||||
app.setAppPath(asarPath);
|
||||
|
||||
if (!process.argv.includes("--vanilla")) {
|
||||
let settings: typeof import("@api/settings").Settings = {} as any;
|
||||
try {
|
||||
settings = JSON.parse(readSettings());
|
||||
} catch { }
|
||||
if (!IS_VANILLA) {
|
||||
const settings = getSettings();
|
||||
|
||||
// Repatch after host updates on Windows
|
||||
if (process.platform === "win32") {
|
||||
@ -83,7 +79,8 @@ if (!process.argv.includes("--vanilla")) {
|
||||
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.backgroundColor = "#00000000";
|
||||
}
|
||||
@ -115,79 +112,6 @@ if (!process.argv.includes("--vanilla")) {
|
||||
);
|
||||
|
||||
process.env.DATA_DIR = join(app.getPath("userData"), "..", "Vencord");
|
||||
|
||||
electron.app.whenReady().then(() => {
|
||||
// Source Maps! Maybe there's a better way but since the renderer is executed
|
||||
// from a string I don't think any other form of sourcemaps would work
|
||||
electron.protocol.registerFileProtocol("vencord", ({ url: unsafeUrl }, cb) => {
|
||||
let url = unsafeUrl.slice("vencord://".length);
|
||||
if (url.endsWith("/")) url = url.slice(0, -1);
|
||||
switch (url) {
|
||||
case "renderer.js.map":
|
||||
case "preload.js.map":
|
||||
case "patcher.js.map": // doubt
|
||||
cb(join(__dirname, url));
|
||||
break;
|
||||
default:
|
||||
cb({ statusCode: 403 });
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
if (settings?.enableReactDevtools)
|
||||
installExt("fmkadmapgofadopljbjfkapdkoienihi")
|
||||
.then(() => console.info("[Vencord] Installed React Developer Tools"))
|
||||
.catch(err => console.error("[Vencord] Failed to install React Developer Tools", err));
|
||||
} catch { }
|
||||
|
||||
|
||||
// Remove CSP
|
||||
type PolicyResult = Record<string, string[]>;
|
||||
|
||||
const parsePolicy = (policy: string): PolicyResult => {
|
||||
const result: PolicyResult = {};
|
||||
policy.split(";").forEach(directive => {
|
||||
const [directiveKey, ...directiveValue] = directive.trim().split(/\s+/g);
|
||||
if (directiveKey && !Object.prototype.hasOwnProperty.call(result, directiveKey)) {
|
||||
result[directiveKey] = directiveValue;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
};
|
||||
const stringifyPolicy = (policy: PolicyResult): string =>
|
||||
Object.entries(policy)
|
||||
.filter(([, values]) => values?.length)
|
||||
.map(directive => directive.flat().join(" "))
|
||||
.join("; ");
|
||||
|
||||
function patchCsp(headers: Record<string, string[]>, header: string) {
|
||||
if (header in headers) {
|
||||
const csp = parsePolicy(headers[header][0]);
|
||||
|
||||
for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src", "worker-src"]) {
|
||||
csp[directive] = ["*", "blob:", "data:", "'unsafe-inline'"];
|
||||
}
|
||||
// TODO: Restrict this to only imported packages with fixed version.
|
||||
// Perhaps auto generate with esbuild
|
||||
csp["script-src"] ??= [];
|
||||
csp["script-src"].push("'unsafe-eval'", "https://unpkg.com", "https://cdnjs.cloudflare.com");
|
||||
headers[header] = [stringifyPolicy(csp)];
|
||||
}
|
||||
}
|
||||
|
||||
electron.session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => {
|
||||
if (responseHeaders) {
|
||||
if (resourceType === "mainFrame")
|
||||
patchCsp(responseHeaders, "content-security-policy");
|
||||
|
||||
// Fix hosts that don't properly set the css content type, such as
|
||||
// raw.githubusercontent.com
|
||||
if (resourceType === "stylesheet")
|
||||
responseHeaders["content-type"] = ["text/css"];
|
||||
}
|
||||
cb({ cancel: false, responseHeaders });
|
||||
});
|
||||
});
|
||||
} else {
|
||||
console.log("[Vencord] Running in vanilla mode. Not loading Vencord");
|
||||
}
|
@ -25,7 +25,7 @@ import { join } from "path";
|
||||
import gitHash from "~git-hash";
|
||||
import gitRemote from "~git-remote";
|
||||
|
||||
import { get } from "../simpleGet";
|
||||
import { get } from "../utils/simpleGet";
|
||||
import { calculateHashes, serializeErrors } from "./common";
|
||||
|
||||
const API_BASE = `https://api.github.com/repos/${gitRemote}`;
|
||||
@ -57,6 +57,13 @@ async function calculateGitChanges() {
|
||||
}));
|
||||
}
|
||||
|
||||
const FILES_TO_DOWNLOAD = [
|
||||
IS_DISCORD_DESKTOP ? "patcher.js" : "vencordDesktopMain.js",
|
||||
"preload.js",
|
||||
IS_DISCORD_DESKTOP ? "renderer.js" : "vencordDesktopRenderer.js",
|
||||
"renderer.css"
|
||||
];
|
||||
|
||||
async function fetchUpdates() {
|
||||
const release = await githubGet("/releases/latest");
|
||||
|
||||
@ -66,7 +73,7 @@ async function fetchUpdates() {
|
||||
return false;
|
||||
|
||||
data.assets.forEach(({ name, browser_download_url }) => {
|
||||
if (["patcher.js", "preload.js", "renderer.js", "renderer.css"].some(s => name.startsWith(s))) {
|
||||
if (FILES_TO_DOWNLOAD.some(s => name.startsWith(s))) {
|
||||
PendingUpdates.push([name, browser_download_url]);
|
||||
}
|
||||
});
|
||||
@ -75,8 +82,17 @@ async function fetchUpdates() {
|
||||
|
||||
async function applyUpdates() {
|
||||
await Promise.all(PendingUpdates.map(
|
||||
async ([name, data]) => writeFile(join(__dirname, name), await get(data)))
|
||||
);
|
||||
async ([name, data]) => writeFile(
|
||||
join(
|
||||
__dirname,
|
||||
IS_VENCORD_DESKTOP
|
||||
// vencordDesktopRenderer.js -> renderer.js
|
||||
? name.replace(/vencordDesktop(\w)/, (_, c) => c.toLowerCase())
|
||||
: name
|
||||
),
|
||||
await get(data)
|
||||
)
|
||||
));
|
||||
PendingUpdates = [];
|
||||
return true;
|
||||
}
|
@ -33,3 +33,5 @@ export const ALLOWED_PROTOCOLS = [
|
||||
"steam:",
|
||||
"spotify:"
|
||||
];
|
||||
|
||||
export const IS_VANILLA = /* @__PURE__ */ process.argv.includes("--vanilla");
|
@ -29,7 +29,7 @@ import { closeModal, Modals, openModal } from "@utils/modal";
|
||||
import definePlugin from "@utils/types";
|
||||
import { Forms } from "@webpack/common";
|
||||
|
||||
const CONTRIBUTOR_BADGE = "https://media.discordapp.net/stickers/1026517526106087454.webp";
|
||||
const CONTRIBUTOR_BADGE = "https://cdn.discordapp.com/attachments/1033680203433660458/1092089947126780035/favicon.png";
|
||||
|
||||
/** List of vencord contributor IDs */
|
||||
const contributorIds: string[] = Object.values(Devs).map(d => d.id.toString());
|
||||
|
@ -1,82 +0,0 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { migratePluginSettings } from "@api/settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
|
||||
// duplicate values have multiple branches with different types. Just include all to be safe
|
||||
const nameMap = {
|
||||
radio: "MenuRadioItem",
|
||||
separator: "MenuSeparator",
|
||||
checkbox: "MenuCheckboxItem",
|
||||
groupstart: "MenuGroup",
|
||||
|
||||
control: "MenuControlItem",
|
||||
compositecontrol: "MenuControlItem",
|
||||
|
||||
item: "MenuItem",
|
||||
customitem: "MenuItem",
|
||||
};
|
||||
|
||||
migratePluginSettings("MenuItemDeobfuscatorAPI", "MenuItemDeobfuscatorApi");
|
||||
export default definePlugin({
|
||||
name: "MenuItemDeobfuscatorAPI",
|
||||
description: "Deobfuscates Discord's Menu Item module",
|
||||
authors: [Devs.Ven],
|
||||
patches: [
|
||||
{
|
||||
find: '"Menu API',
|
||||
replacement: {
|
||||
match: /function.{0,80}type===(\i)\).{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,12 +22,12 @@ import definePlugin from "@utils/types";
|
||||
export default definePlugin({
|
||||
name: "MessagePopoverAPI",
|
||||
description: "API to add buttons to message popovers.",
|
||||
authors: [Devs.KingFish, Devs.Ven],
|
||||
authors: [Devs.KingFish, Devs.Ven, Devs.Nuckyz],
|
||||
patches: [{
|
||||
find: "Messages.MESSAGE_UTILITIES_A11Y_LABEL",
|
||||
replacement: {
|
||||
// foo && !bar ? createElement(blah,...makeElement(addReactionData))
|
||||
match: /\i&&!\i\?\(0,\i\.jsxs?\)\(.{0,200}renderPopout:.{0,300}?(\i)\(.{3,20}\{key:"add-reaction".+?\}/,
|
||||
// foo && !bar ? createElement(reactionStuffs)... createElement(blah,...makeElement(reply-other))
|
||||
match: /\i&&!\i\?\(0,\i\.jsxs?\)\(.{0,200}renderEmojiPicker:.{0,500}\?(\i)\(\{key:"reply-other"/,
|
||||
replace: (m, makeElement) => {
|
||||
const msg = m.match(/message:(.{1,3}),/)?.[1];
|
||||
if (!msg) throw new Error("Could not find message variable");
|
||||
|
38
src/plugins/apiSettingsStore.ts
Normal file
38
src/plugins/apiSettingsStore.ts
Normal file
@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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]$&"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
84
src/plugins/betterFolders/FolderSideBar.tsx
Normal file
84
src/plugins/betterFolders/FolderSideBar.tsx
Normal file
@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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 });
|
17
src/plugins/betterFolders/betterFolders.css
Normal file
17
src/plugins/betterFolders/betterFolders.css
Normal file
@ -0,0 +1,17 @@
|
||||
.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;
|
||||
}
|
177
src/plugins/betterFolders/index.ts
Normal file
177
src/plugins/betterFolders/index.ts
Normal file
@ -0,0 +1,177 @@
|
||||
/*
|
||||
* 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],
|
||||
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.guildFolders.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);
|
||||
},
|
||||
});
|
@ -44,7 +44,7 @@ export default definePlugin({
|
||||
match: /"(?:username|dot)"===\i(?!\.\i)/g,
|
||||
replace: "true",
|
||||
},
|
||||
},
|
||||
}
|
||||
],
|
||||
|
||||
options: {
|
||||
|
@ -17,6 +17,7 @@
|
||||
*/
|
||||
|
||||
import { Devs } from "@utils/constants";
|
||||
import { relaunch } from "@utils/native";
|
||||
import definePlugin from "@utils/types";
|
||||
import * as Webpack from "@webpack";
|
||||
import { extract, filters, findAll, search } from "@webpack";
|
||||
@ -71,13 +72,14 @@ export default definePlugin({
|
||||
findAllByProps: (...props: string[]) => findAll(filters.byProps(...props)),
|
||||
findByCode: newFindWrapper(filters.byCode),
|
||||
findAllByCode: (code: string) => findAll(filters.byCode(code)),
|
||||
findStore: newFindWrapper(filters.byStoreName),
|
||||
PluginsApi: Vencord.Plugins,
|
||||
plugins: Vencord.Plugins.plugins,
|
||||
React,
|
||||
Settings: Vencord.Settings,
|
||||
Api: Vencord.Api,
|
||||
reload: () => location.reload(),
|
||||
restart: IS_WEB ? WEB_ONLY("restart") : window.DiscordNative.app.relaunch
|
||||
restart: IS_WEB ? WEB_ONLY("restart") : relaunch
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -1,105 +0,0 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { ApplicationCommandOptionType, sendBotMessage } from "@api/Commands";
|
||||
import { findOption } from "@api/Commands/commandHelpers";
|
||||
import { ApplicationCommandInputType } from "@api/Commands/types";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import { findByCode, findByProps } from "@webpack";
|
||||
|
||||
const DRAFT_TYPE = 0;
|
||||
|
||||
export default definePlugin({
|
||||
name: "CorruptMp4s",
|
||||
description: "Create corrupt mp4s with extremely high or negative duration",
|
||||
authors: [Devs.Ven],
|
||||
dependencies: ["CommandsAPI"],
|
||||
commands: [{
|
||||
name: "corrupt",
|
||||
description: "Create a corrupt mp4 with extremely high or negative duration",
|
||||
inputType: ApplicationCommandInputType.BUILT_IN,
|
||||
options: [
|
||||
{
|
||||
name: "mp4",
|
||||
description: "the video to corrupt",
|
||||
type: ApplicationCommandOptionType.ATTACHMENT,
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: "kind",
|
||||
description: "the kind of corruption",
|
||||
type: ApplicationCommandOptionType.STRING,
|
||||
choices: [
|
||||
{
|
||||
name: "infinite",
|
||||
value: "infinite",
|
||||
label: "Very high duration"
|
||||
},
|
||||
{
|
||||
name: "negative",
|
||||
value: "negative",
|
||||
label: "Negative duration"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
execute: async (args, ctx) => {
|
||||
const UploadStore = findByProps("getUploads");
|
||||
const upload = UploadStore.getUploads(ctx.channel.id, DRAFT_TYPE)[0];
|
||||
|
||||
const video = upload?.item?.file as File | undefined;
|
||||
|
||||
if (video?.type !== "video/mp4")
|
||||
return void sendBotMessage(ctx.channel.id, {
|
||||
content: "Please upload a mp4 file"
|
||||
});
|
||||
|
||||
const corruption = findOption<string>(args, "kind", "infinite");
|
||||
|
||||
const buf = new Uint8Array(await video.arrayBuffer());
|
||||
let found = false;
|
||||
|
||||
// adapted from https://github.com/GeopJr/exorcism/blob/c9a12d77ccbcb49c987b385eafae250906efc297/src/App.svelte#L41-L48
|
||||
for (let i = 0; i < buf.length; i++) {
|
||||
if (buf[i] === 0x6d && buf[i + 1] === 0x76 && buf[i + 2] === 0x68 && buf[i + 3] === 0x64) {
|
||||
let start = i + 18;
|
||||
buf[start++] = 0x00;
|
||||
buf[start++] = 0x01;
|
||||
buf[start++] = corruption === "negative" ? 0xff : 0x7f;
|
||||
buf[start++] = 0xff;
|
||||
buf[start++] = 0xff;
|
||||
buf[start++] = corruption === "negative" ? 0xf0 : 0xff;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
return void sendBotMessage(ctx.channel.id, {
|
||||
content: "Could not find signature. Is this even a mp4?"
|
||||
});
|
||||
}
|
||||
|
||||
const newName = video.name.replace(/\.mp4$/i, ".corrupt.mp4");
|
||||
const promptToUpload = findByCode("UPLOAD_FILE_LIMIT_ERROR");
|
||||
const file = new File([buf], newName, { type: "video/mp4" });
|
||||
setTimeout(() => promptToUpload([file], ctx.channel, DRAFT_TYPE), 10);
|
||||
}
|
||||
}]
|
||||
});
|
@ -78,6 +78,7 @@ export default definePlugin({
|
||||
color: "#eed202",
|
||||
title: "Discord has crashed!",
|
||||
body: "Awn :( Discord has crashed more than five times, not attempting to recover.",
|
||||
noPersist: true,
|
||||
});
|
||||
} catch { }
|
||||
|
||||
@ -111,6 +112,7 @@ export default definePlugin({
|
||||
color: "#eed202",
|
||||
title: "Discord has crashed!",
|
||||
body: "Attempting to recover...",
|
||||
noPersist: true,
|
||||
});
|
||||
} catch { }
|
||||
}
|
||||
|
@ -116,7 +116,8 @@ function initWs(isManual = false) {
|
||||
showNotification({
|
||||
title: "Dev Companion Error",
|
||||
body: (e as ErrorEvent).message || "No Error Message",
|
||||
color: "var(--status-danger, red)"
|
||||
color: "var(--status-danger, red)",
|
||||
noPersist: true,
|
||||
});
|
||||
});
|
||||
|
||||
@ -128,7 +129,8 @@ function initWs(isManual = false) {
|
||||
showNotification({
|
||||
title: "Dev Companion Disconnected",
|
||||
body: e.reason || "No Reason provided",
|
||||
color: "var(--status-danger, red)"
|
||||
color: "var(--status-danger, red)",
|
||||
noPersist: true,
|
||||
});
|
||||
});
|
||||
|
||||
@ -159,7 +161,11 @@ function initWs(isManual = false) {
|
||||
return reply("Expected exactly one 'find' matches, found " + keys.length);
|
||||
|
||||
const mod = candidates[keys[0]];
|
||||
let src = String(mod.original ?? mod);
|
||||
let src = String(mod.original ?? mod).replaceAll("\n", "");
|
||||
|
||||
if (src.startsWith("function(")) {
|
||||
src = "0," + src;
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
|
||||
|
@ -238,7 +238,7 @@ 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],
|
||||
dependencies: ["MenuItemDeobfuscatorAPI", "ContextMenuAPI"],
|
||||
dependencies: ["ContextMenuAPI"],
|
||||
|
||||
start() {
|
||||
addContextMenuPatch("message", messageContextMenuPatch);
|
||||
|
@ -204,7 +204,7 @@ export default definePlugin({
|
||||
},
|
||||
{
|
||||
match: /(?=return{hasSpoilerEmbeds:\i,content:(\i)})/,
|
||||
replace: (_, content) => `${content}=$self.patchFakeNitroEmojis(${content});`
|
||||
replace: (_, content) => `${content}=$self.patchFakeNitroEmojis(${content},arguments[2]?.formatInline);`
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -333,7 +333,7 @@ export default definePlugin({
|
||||
|
||||
EmojiComponent: null as any,
|
||||
|
||||
patchFakeNitroEmojis(content: Array<any>) {
|
||||
patchFakeNitroEmojis(content: Array<any>, inline: boolean) {
|
||||
if (!this.EmojiComponent) return content;
|
||||
|
||||
const newContent: Array<any> = [];
|
||||
@ -353,7 +353,7 @@ export default definePlugin({
|
||||
newContent.push((
|
||||
<this.EmojiComponent node={{
|
||||
type: "customEmoji",
|
||||
jumboable: content.length === 1,
|
||||
jumboable: !inline && content.length === 1,
|
||||
animated: fakeNitroMatch[2] === "gif",
|
||||
name: ":FakeNitroEmoji:",
|
||||
emojiId: fakeNitroMatch[1]
|
||||
|
145
src/plugins/fakeProfileThemes.tsx
Normal file
145
src/plugins/fakeProfileThemes.tsx
Normal file
@ -0,0 +1,145 @@
|
||||
/*
|
||||
* 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 upsells, 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 }),
|
||||
});
|
85
src/plugins/gameActivityToggle/index.tsx
Normal file
85
src/plugins/gameActivityToggle/index.tsx
Normal file
@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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="Toggle 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);
|
||||
}
|
||||
});
|
3
src/plugins/gameActivityToggle/style.css
Normal file
3
src/plugins/gameActivityToggle/style.css
Normal file
@ -0,0 +1,3 @@
|
||||
[class*="withTagAsButton"] {
|
||||
min-width: 88px;
|
||||
}
|
47
src/plugins/gifPaste.ts
Normal file
47
src/plugins/gifPaste.ts
Normal file
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import { filters, findLazy, mapMangledModuleLazy } from "@webpack";
|
||||
|
||||
const ExpressionPickerState = mapMangledModuleLazy('name:"expression-picker-last-active-view"', {
|
||||
close: filters.byCode("activeView:null", "setState")
|
||||
});
|
||||
const ComponentDispatch = findLazy(m => m.emitter?._events?.INSERT_TEXT);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
});
|
@ -147,8 +147,8 @@ export default definePlugin({
|
||||
{
|
||||
find: ".Messages.SETTINGS_GAMES_TOGGLE_OVERLAY",
|
||||
replacement: {
|
||||
match: /!(\i)\|\|(null==\i\)return null;var \i=(\i)\.overlay.+?children:)(\[.{0,70}overlayStatusText.+?\])(?=}\)}\(\))/,
|
||||
replace: (_, platformCheck, restWithoutPlatformCheck, props, children) => ""
|
||||
match: /!(\i)(\)return null;var \i=(\i)\.overlay.+?children:)(\[.{0,70}overlayStatusText.+?\])(?=}\)}\(\))/,
|
||||
replace: (_, platformCheck, restWithoutPlatformCheck, props, children) => "false"
|
||||
+ `${restWithoutPlatformCheck}`
|
||||
+ `(${platformCheck}?${children}:[])`
|
||||
+ `.concat(Vencord.Plugins.plugins.IgnoreActivities.renderToggleGameActivityButton(${props}))`
|
||||
|
@ -76,7 +76,7 @@ export default definePlugin({
|
||||
name: "MessageLogger",
|
||||
description: "Temporarily logs deleted and edited messages.",
|
||||
authors: [Devs.rushii, Devs.Ven],
|
||||
dependencies: ["ContextMenuAPI", "MenuItemDeobfuscatorAPI"],
|
||||
dependencies: ["ContextMenuAPI"],
|
||||
|
||||
start() {
|
||||
addDeleteStyle();
|
||||
@ -209,6 +209,11 @@ export default definePlugin({
|
||||
" m" +
|
||||
")" +
|
||||
".update($3"
|
||||
},
|
||||
{
|
||||
// fix up key (edit last message) attempting to edit a deleted message
|
||||
match: /(?<=getLastEditableMessage=.{0,200}\.find\(\(function\((\i)\)\{)return/,
|
||||
replace: "return !$1.deleted &&"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -21,8 +21,8 @@ import definePlugin from "@utils/types";
|
||||
|
||||
export default definePlugin({
|
||||
name: "NoTrack",
|
||||
description: "Disable Discord's tracking and crash reporting",
|
||||
authors: [Devs.Cyn],
|
||||
description: "Disable Discord's tracking ('science'), metrics and Sentry crash reporting",
|
||||
authors: [Devs.Cyn, Devs.Ven, Devs.Nuckyz],
|
||||
required: true,
|
||||
patches: [
|
||||
{
|
||||
@ -35,8 +35,8 @@ export default definePlugin({
|
||||
{
|
||||
find: "window.DiscordSentry=",
|
||||
replacement: {
|
||||
match: /window\.DiscordSentry=function.+\}\(\)/,
|
||||
replace: "",
|
||||
match: /^.+$/,
|
||||
replace: "()=>{}",
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -63,8 +63,8 @@ export default definePlugin({
|
||||
{
|
||||
find: ".Messages.USER_POPOUT_PRONOUNS",
|
||||
replacement: {
|
||||
match: /\i\.\i\.useExperiment\({}\)\.showPronouns/,
|
||||
replace: "true"
|
||||
match: /\.showPronouns/,
|
||||
replace: ".showPronouns||true"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
@ -111,7 +111,7 @@ function jumpIfOffScreen(channelId: string, messageId: string) {
|
||||
}
|
||||
|
||||
function getNextMessage(isUp: boolean, isReply: boolean) {
|
||||
let messages: Message[] = MessageStore.getMessages(SelectedChannelStore.getChannelId())._array;
|
||||
let messages: Array<Message & { deleted?: boolean; }> = MessageStore.getMessages(SelectedChannelStore.getChannelId())._array;
|
||||
if (!isReply) { // we are editing so only include own
|
||||
const meId = UserStore.getCurrentUser().id;
|
||||
messages = messages.filter(m => m.author.id === meId);
|
||||
@ -121,11 +121,18 @@ function getNextMessage(isUp: boolean, isReply: boolean) {
|
||||
? Math.min(messages.length - 1, i + 1)
|
||||
: Math.max(-1, i - 1);
|
||||
|
||||
const findNextNonDeleted = (i: number) => {
|
||||
do {
|
||||
i = mutate(i);
|
||||
} while (i !== -1 && messages[messages.length - i - 1]?.deleted === true);
|
||||
return i;
|
||||
};
|
||||
|
||||
let i: number;
|
||||
if (isReply)
|
||||
replyIdx = i = mutate(replyIdx);
|
||||
replyIdx = i = findNextNonDeleted(replyIdx);
|
||||
else
|
||||
editIdx = i = mutate(editIdx);
|
||||
editIdx = i = findNextNonDeleted(editIdx);
|
||||
|
||||
return i === - 1 ? undefined : messages[messages.length - i - 1];
|
||||
}
|
||||
|
@ -62,10 +62,10 @@ export default definePlugin({
|
||||
renderReadAllButton: () => <ReadAllButton />,
|
||||
|
||||
start() {
|
||||
addServerListElement(ServerListRenderPosition.In, this.renderReadAllButton);
|
||||
addServerListElement(ServerListRenderPosition.Above, this.renderReadAllButton);
|
||||
},
|
||||
|
||||
stop() {
|
||||
removeServerListElement(ServerListRenderPosition.In, this.renderReadAllButton);
|
||||
removeServerListElement(ServerListRenderPosition.Above, this.renderReadAllButton);
|
||||
}
|
||||
});
|
||||
|
40
src/plugins/relationshipNotifier/events.ts
Normal file
40
src/plugins/relationshipNotifier/events.ts
Normal file
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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 { FluxEvents } from "@webpack/types";
|
||||
|
||||
import { onChannelDelete, onGuildDelete, onRelationshipRemove } from "./functions";
|
||||
import { syncFriends, syncGroups, syncGuilds } from "./utils";
|
||||
|
||||
export const FluxHandlers: Partial<Record<FluxEvents, Array<(data: any) => void>>> = {
|
||||
GUILD_CREATE: [syncGuilds],
|
||||
GUILD_DELETE: [onGuildDelete],
|
||||
CHANNEL_CREATE: [syncGroups],
|
||||
CHANNEL_DELETE: [onChannelDelete],
|
||||
RELATIONSHIP_ADD: [syncFriends],
|
||||
RELATIONSHIP_UPDATE: [syncFriends],
|
||||
RELATIONSHIP_REMOVE: [syncFriends, onRelationshipRemove]
|
||||
};
|
||||
|
||||
export function forEachEvent(fn: (event: FluxEvents, handler: (data: any) => void) => void) {
|
||||
for (const event in FluxHandlers) {
|
||||
for (const cb of FluxHandlers[event]) {
|
||||
fn(event as FluxEvents, cb);
|
||||
}
|
||||
}
|
||||
}
|
87
src/plugins/relationshipNotifier/functions.ts
Normal file
87
src/plugins/relationshipNotifier/functions.ts
Normal file
@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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 { UserUtils } from "@webpack/common";
|
||||
|
||||
import settings from "./settings";
|
||||
import { ChannelDelete, ChannelType, GuildDelete, RelationshipRemove, RelationshipType } from "./types";
|
||||
import { deleteGroup, deleteGuild, getGroup, getGuild, notify } from "./utils";
|
||||
|
||||
let manuallyRemovedFriend: string | undefined;
|
||||
let manuallyRemovedGuild: string | undefined;
|
||||
let manuallyRemovedGroup: string | undefined;
|
||||
|
||||
export const removeFriend = (id: string) => manuallyRemovedFriend = id;
|
||||
export const removeGuild = (id: string) => manuallyRemovedGuild = id;
|
||||
export const removeGroup = (id: string) => manuallyRemovedGroup = id;
|
||||
|
||||
export async function onRelationshipRemove({ relationship: { type, id } }: RelationshipRemove) {
|
||||
if (manuallyRemovedFriend === id) {
|
||||
manuallyRemovedFriend = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await UserUtils.fetchUser(id)
|
||||
.catch(() => null);
|
||||
if (!user) return;
|
||||
|
||||
switch (type) {
|
||||
case RelationshipType.FRIEND:
|
||||
if (settings.store.friends)
|
||||
notify(`${user.tag} removed you as a friend.`, user.getAvatarURL(undefined, undefined, false));
|
||||
break;
|
||||
case RelationshipType.FRIEND_REQUEST:
|
||||
if (settings.store.friendRequestCancels)
|
||||
notify(`A friend request from ${user.tag} has been removed.`, user.getAvatarURL(undefined, undefined, false));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
export function onGuildDelete({ guild: { id, unavailable } }: GuildDelete) {
|
||||
if (!settings.store.servers) return;
|
||||
if (unavailable) return;
|
||||
|
||||
if (manuallyRemovedGuild === id) {
|
||||
deleteGuild(id);
|
||||
manuallyRemovedGuild = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
const guild = getGuild(id);
|
||||
if (guild) {
|
||||
deleteGuild(id);
|
||||
notify(`You were removed from the server ${guild.name}.`, guild.iconURL);
|
||||
}
|
||||
}
|
||||
|
||||
export function onChannelDelete({ channel: { id, type } }: ChannelDelete) {
|
||||
if (!settings.store.groups) return;
|
||||
if (type !== ChannelType.GROUP_DM) return;
|
||||
|
||||
if (manuallyRemovedGroup === id) {
|
||||
deleteGroup(id);
|
||||
manuallyRemovedGroup = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
const group = getGroup(id);
|
||||
if (group) {
|
||||
deleteGroup(id);
|
||||
notify(`You were removed from the group ${group.name}.`, group.iconURL);
|
||||
}
|
||||
}
|
72
src/plugins/relationshipNotifier/index.ts
Normal file
72
src/plugins/relationshipNotifier/index.ts
Normal file
@ -0,0 +1,72 @@
|
||||
/*
|
||||
* 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 { FluxDispatcher } from "@webpack/common";
|
||||
|
||||
import { forEachEvent } from "./events";
|
||||
import { removeFriend, removeGroup, removeGuild } from "./functions";
|
||||
import settings from "./settings";
|
||||
import { syncAndRunChecks } from "./utils";
|
||||
|
||||
export default definePlugin({
|
||||
name: "RelationshipNotifier",
|
||||
description: "Notifies you when a friend, group chat, or server removes you.",
|
||||
authors: [Devs.nick],
|
||||
settings,
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: "removeRelationship:function(",
|
||||
replacement: {
|
||||
match: /(removeRelationship:function\((\i),\i,\i\){)/,
|
||||
replace: "$1$self.removeFriend($2);"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: "leaveGuild:function(",
|
||||
replacement: {
|
||||
match: /(leaveGuild:function\((\i)\){)/,
|
||||
replace: "$1$self.removeGuild($2);"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: "closePrivateChannel:function(",
|
||||
replacement: {
|
||||
match: /(closePrivateChannel:function\((\i)\){)/,
|
||||
replace: "$1$self.removeGroup($2);"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
async start() {
|
||||
setTimeout(() => {
|
||||
syncAndRunChecks();
|
||||
}, 5000);
|
||||
forEachEvent((ev, cb) => FluxDispatcher.subscribe(ev, cb));
|
||||
},
|
||||
|
||||
stop() {
|
||||
forEachEvent((ev, cb) => FluxDispatcher.unsubscribe(ev, cb));
|
||||
},
|
||||
|
||||
removeFriend,
|
||||
removeGroup,
|
||||
removeGuild
|
||||
});
|
53
src/plugins/relationshipNotifier/settings.ts
Normal file
53
src/plugins/relationshipNotifier/settings.ts
Normal file
@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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 { definePluginSettings } from "@api/settings";
|
||||
import { OptionType } from "@utils/types";
|
||||
|
||||
export default definePluginSettings({
|
||||
notices: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Also show a notice at the top of your screen when removed (use this if you don't want to miss any notifications).",
|
||||
default: false
|
||||
},
|
||||
offlineRemovals: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Notify you when starting discord if you were removed while offline.",
|
||||
default: true
|
||||
},
|
||||
friends: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Notify when a friend removes you",
|
||||
default: true
|
||||
},
|
||||
friendRequestCancels: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Notify when a friend request is cancelled",
|
||||
default: true
|
||||
},
|
||||
servers: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Notify when removed from a server",
|
||||
default: true
|
||||
},
|
||||
groups: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Notify when removed from a group chat",
|
||||
default: true
|
||||
}
|
||||
});
|
62
src/plugins/relationshipNotifier/types.ts
Normal file
62
src/plugins/relationshipNotifier/types.ts
Normal file
@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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 { Channel } from "discord-types/general";
|
||||
|
||||
export interface ChannelDelete {
|
||||
type: "CHANNEL_DELETE";
|
||||
channel: Channel;
|
||||
}
|
||||
|
||||
export interface GuildDelete {
|
||||
type: "GUILD_DELETE";
|
||||
guild: {
|
||||
id: string;
|
||||
unavailable?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface RelationshipRemove {
|
||||
type: "RELATIONSHIP_REMOVE";
|
||||
relationship: {
|
||||
id: string;
|
||||
nickname: string;
|
||||
type: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SimpleGroupChannel {
|
||||
id: string;
|
||||
name: string;
|
||||
iconURL?: string;
|
||||
}
|
||||
|
||||
export interface SimpleGuild {
|
||||
id: string;
|
||||
name: string;
|
||||
iconURL?: string;
|
||||
}
|
||||
|
||||
export const enum ChannelType {
|
||||
GROUP_DM = 3,
|
||||
}
|
||||
|
||||
export const enum RelationshipType {
|
||||
FRIEND = 1,
|
||||
FRIEND_REQUEST = 3,
|
||||
}
|
149
src/plugins/relationshipNotifier/utils.ts
Normal file
149
src/plugins/relationshipNotifier/utils.ts
Normal file
@ -0,0 +1,149 @@
|
||||
/*
|
||||
* 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 { DataStore, Notices } from "@api/index";
|
||||
import { showNotification } from "@api/Notifications";
|
||||
import { ChannelStore, GuildStore, RelationshipStore, UserUtils } from "@webpack/common";
|
||||
|
||||
import settings from "./settings";
|
||||
import { ChannelType, RelationshipType, SimpleGroupChannel, SimpleGuild } from "./types";
|
||||
|
||||
const guilds = new Map<string, SimpleGuild>();
|
||||
const groups = new Map<string, SimpleGroupChannel>();
|
||||
const friends = {
|
||||
friends: [] as string[],
|
||||
requests: [] as string[]
|
||||
};
|
||||
|
||||
export async function syncAndRunChecks() {
|
||||
const [oldGuilds, oldGroups, oldFriends] = await DataStore.getMany([
|
||||
"relationship-notifier-guilds",
|
||||
"relationship-notifier-groups",
|
||||
"relationship-notifier-friends"
|
||||
]) as [Map<string, SimpleGuild> | undefined, Map<string, SimpleGroupChannel> | undefined, Record<"friends" | "requests", string[]> | undefined];
|
||||
|
||||
await Promise.all([syncGuilds(), syncGroups(), syncFriends()]);
|
||||
|
||||
if (settings.store.offlineRemovals) {
|
||||
if (settings.store.groups && oldGroups?.size) {
|
||||
for (const [id, group] of oldGroups) {
|
||||
if (!groups.has(id))
|
||||
notify(`You are no longer in the group ${group.name}.`, group.iconURL);
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.store.servers && oldGuilds?.size) {
|
||||
for (const [id, guild] of oldGuilds) {
|
||||
if (!guilds.has(id))
|
||||
notify(`You are no longer in the server ${guild.name}.`, guild.iconURL);
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.store.friends && oldFriends?.friends.length) {
|
||||
for (const id of oldFriends.friends) {
|
||||
if (friends.friends.includes(id)) continue;
|
||||
|
||||
const user = await UserUtils.fetchUser(id).catch(() => void 0);
|
||||
if (user)
|
||||
notify(`You are no longer friends with ${user.tag}.`, user.getAvatarURL(undefined, undefined, false));
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.store.friendRequestCancels && oldFriends?.requests?.length) {
|
||||
for (const id of oldFriends.requests) {
|
||||
if (friends.requests.includes(id)) continue;
|
||||
|
||||
const user = await UserUtils.fetchUser(id).catch(() => void 0);
|
||||
if (user)
|
||||
notify(`Friend request from ${user.tag} has been revoked.`, user.getAvatarURL(undefined, undefined, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function notify(text: string, icon?: string) {
|
||||
if (settings.store.notices)
|
||||
Notices.showNotice(text, "OK", () => Notices.popNotice());
|
||||
|
||||
showNotification({
|
||||
title: "Relationship Notifier",
|
||||
body: text,
|
||||
icon
|
||||
});
|
||||
}
|
||||
|
||||
export function getGuild(id: string) {
|
||||
return guilds.get(id);
|
||||
}
|
||||
|
||||
export function deleteGuild(id: string) {
|
||||
guilds.delete(id);
|
||||
syncGuilds();
|
||||
}
|
||||
|
||||
export async function syncGuilds() {
|
||||
for (const [id, { name, icon }] of Object.entries(GuildStore.getGuilds())) {
|
||||
guilds.set(id, {
|
||||
id,
|
||||
name,
|
||||
iconURL: icon && `https://cdn.discordapp.com/icons/${id}/${icon}.png`
|
||||
});
|
||||
}
|
||||
await DataStore.set("relationship-notifier-guilds", guilds);
|
||||
}
|
||||
|
||||
export function getGroup(id: string) {
|
||||
return groups.get(id);
|
||||
}
|
||||
|
||||
export function deleteGroup(id: string) {
|
||||
groups.delete(id);
|
||||
syncGroups();
|
||||
}
|
||||
|
||||
export async function syncGroups() {
|
||||
for (const { type, id, name, rawRecipients, icon } of ChannelStore.getSortedPrivateChannels()) {
|
||||
if (type === ChannelType.GROUP_DM)
|
||||
groups.set(id, {
|
||||
id,
|
||||
name: name || rawRecipients.map(r => r.username).join(", "),
|
||||
iconURL: icon && `https://cdn.discordapp.com/channel-icons/${id}/${icon}.png`
|
||||
});
|
||||
}
|
||||
|
||||
await DataStore.set("relationship-notifier-groups", groups);
|
||||
}
|
||||
|
||||
export async function syncFriends() {
|
||||
friends.friends = [];
|
||||
friends.requests = [];
|
||||
|
||||
const relationShips = RelationshipStore.getRelationships();
|
||||
for (const id in relationShips) {
|
||||
switch (relationShips[id]) {
|
||||
case RelationshipType.FRIEND:
|
||||
friends.friends.push(id);
|
||||
break;
|
||||
case RelationshipType.FRIEND_REQUEST:
|
||||
friends.requests.push(id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await DataStore.set("relationship-notifier-friends", friends);
|
||||
}
|
@ -76,7 +76,7 @@ export default definePlugin({
|
||||
name: "ReverseImageSearch",
|
||||
description: "Adds ImageSearch to image context menus",
|
||||
authors: [Devs.Ven, Devs.Nuckyz],
|
||||
dependencies: ["MenuItemDeobfuscatorAPI", "ContextMenuAPI"],
|
||||
dependencies: ["ContextMenuAPI"],
|
||||
patches: [
|
||||
{
|
||||
find: ".Messages.MESSAGE_ACTIONS_MENU_LABEL",
|
||||
|
@ -168,6 +168,7 @@ export default definePlugin({
|
||||
get additionalInfo() {
|
||||
if (IS_DEV) return " (Dev)";
|
||||
if (IS_WEB) return " (Web)";
|
||||
if (IS_VENCORD_DESKTOP) return " (Vencord Desktop)";
|
||||
if (IS_STANDALONE) return " (Standalone)";
|
||||
return "";
|
||||
},
|
||||
|
@ -22,8 +22,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { Link } from "@components/Link";
|
||||
import { debounce } from "@utils/debounce";
|
||||
import { classes, LazyComponent } from "@utils/misc";
|
||||
import { filters, find } from "@webpack";
|
||||
import { classes, copyWithToast } from "@utils/misc";
|
||||
import { ContextMenu, FluxDispatcher, Forms, Menu, React, useEffect, useState, useStateFromStores } from "@webpack/common";
|
||||
|
||||
import { SpotifyStore, Track } from "./SpotifyStore";
|
||||
@ -74,6 +73,37 @@ function Button(props: React.ButtonHTMLAttributes<HTMLButtonElement>) {
|
||||
);
|
||||
}
|
||||
|
||||
function CopyContextMenu({ name, path }: { name: string; path: string; }) {
|
||||
const copyId = `spotify-copy-${name}`;
|
||||
const openId = `spotify-open-${name}`;
|
||||
|
||||
return (
|
||||
<Menu.Menu
|
||||
navId={`spotify-${name}-menu`}
|
||||
onClose={() => FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
|
||||
aria-label={`Spotify ${name} Menu`}
|
||||
>
|
||||
<Menu.MenuItem
|
||||
key={copyId}
|
||||
id={copyId}
|
||||
label={`Copy ${name} Link`}
|
||||
action={() => copyWithToast("https://open.spotify.com" + path)}
|
||||
/>
|
||||
<Menu.MenuItem
|
||||
key={openId}
|
||||
id={openId}
|
||||
label={`Open ${name} in Spotify`}
|
||||
action={() => SpotifyStore.openExternal(path)}
|
||||
/>
|
||||
</Menu.Menu>
|
||||
);
|
||||
}
|
||||
|
||||
function makeContextMenu(name: string, path: string) {
|
||||
return (e: React.MouseEvent<HTMLElement, MouseEvent>) =>
|
||||
ContextMenu.open(e, () => <CopyContextMenu name={name} path={path} />);
|
||||
}
|
||||
|
||||
function Controls() {
|
||||
const [isPlaying, shuffle, repeat] = useStateFromStores(
|
||||
[SpotifyStore],
|
||||
@ -123,11 +153,6 @@ const seek = debounce((v: number) => {
|
||||
SpotifyStore.seek(v);
|
||||
});
|
||||
|
||||
const Slider = LazyComponent(() => {
|
||||
const filter = filters.byCode("sliderContainer");
|
||||
return find(m => m.render && filter(m.render));
|
||||
});
|
||||
|
||||
function SeekBar() {
|
||||
const { duration } = SpotifyStore.track!;
|
||||
|
||||
@ -159,7 +184,7 @@ function SeekBar() {
|
||||
>
|
||||
{msToHuman(position)}
|
||||
</Forms.FormText>
|
||||
<Slider
|
||||
<Menu.MenuSliderControl
|
||||
minValue={0}
|
||||
maxValue={duration}
|
||||
value={position}
|
||||
@ -186,7 +211,7 @@ function AlbumContextMenu({ track }: { track: Track; }) {
|
||||
const volume = useStateFromStores([SpotifyStore], () => SpotifyStore.volume);
|
||||
|
||||
return (
|
||||
<Menu.ContextMenu
|
||||
<Menu.Menu
|
||||
navId="spotify-album-menu"
|
||||
onClose={() => FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
|
||||
aria-label="Spotify Album Menu"
|
||||
@ -209,7 +234,7 @@ function AlbumContextMenu({ track }: { track: Track; }) {
|
||||
key="spotify-volume"
|
||||
label="Volume"
|
||||
control={(props, ref) => (
|
||||
<Slider
|
||||
<Menu.MenuSliderControl
|
||||
{...props}
|
||||
ref={ref}
|
||||
value={volume}
|
||||
@ -219,7 +244,7 @@ function AlbumContextMenu({ track }: { track: Track; }) {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Menu.ContextMenu>
|
||||
</Menu.Menu>
|
||||
);
|
||||
}
|
||||
|
||||
@ -263,6 +288,7 @@ function Info({ track }: { track: Track; }) {
|
||||
onClick={track.id ? () => {
|
||||
SpotifyStore.openExternal(`/track/${track.id}`);
|
||||
} : void 0}
|
||||
onContextMenu={track.id ? makeContextMenu("Song", `/track/${track.id}`) : void 0}
|
||||
>
|
||||
{track.name}
|
||||
</Forms.FormText>
|
||||
@ -277,6 +303,7 @@ function Info({ track }: { track: Track; }) {
|
||||
href={`https://open.spotify.com/artist/${a.id}`}
|
||||
style={{ fontSize: "inherit" }}
|
||||
title={a.name}
|
||||
onContextMenu={makeContextMenu("Artist", `/artist/${a.id}`)}
|
||||
>
|
||||
{a.name}
|
||||
</Link>
|
||||
@ -295,6 +322,7 @@ function Info({ track }: { track: Track; }) {
|
||||
disabled={!track.album.id}
|
||||
style={{ fontSize: "inherit" }}
|
||||
title={track.album.name}
|
||||
onContextMenu={makeContextMenu("Album", `/album/${track.album.id}`)}
|
||||
>
|
||||
{track.album.name}
|
||||
</Link>
|
||||
@ -338,10 +366,10 @@ export function Player() {
|
||||
|
||||
return (
|
||||
<ErrorBoundary fallback={() => (
|
||||
<>
|
||||
<Forms.FormText>Failed to render Spotify Modal :(</Forms.FormText>
|
||||
<Forms.FormText>Check the console for errors</Forms.FormText>
|
||||
</>
|
||||
<div className="vc-spotify-fallback">
|
||||
<p>Failed to render Spotify Modal :(</p>
|
||||
<p >Check the console for errors</p>
|
||||
</div>
|
||||
)}>
|
||||
<div id={cl("player")}>
|
||||
<Info track={track} />
|
||||
|
@ -16,6 +16,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Settings } from "@api/settings";
|
||||
import IpcEvents from "@utils/IpcEvents";
|
||||
import { proxyLazy } from "@utils/proxyLazy";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
@ -89,7 +90,11 @@ export const SpotifyStore = proxyLazy(() => {
|
||||
public isSettingPosition = false;
|
||||
|
||||
public openExternal(path: string) {
|
||||
VencordNative.ipc.invoke(IpcEvents.OPEN_EXTERNAL, "https://open.spotify.com" + path);
|
||||
const url = Settings.plugins.SpotifyControls.useSpotifyUris
|
||||
? "spotify:" + path.replaceAll("/", (_, idx) => idx === 0 ? "" : ":")
|
||||
: "https://open.spotify.com" + path;
|
||||
|
||||
VencordNative.ipc.invoke(IpcEvents.OPEN_EXTERNAL, url);
|
||||
}
|
||||
|
||||
// Need to keep track of this manually
|
||||
|
@ -39,7 +39,6 @@ export default definePlugin({
|
||||
name: "SpotifyControls",
|
||||
description: "Spotify Controls",
|
||||
authors: [Devs.Ven, Devs.afn, Devs.KraXen72],
|
||||
dependencies: ["MenuItemDeobfuscatorAPI"],
|
||||
options: {
|
||||
hoverControls: {
|
||||
description: "Show controls on hover",
|
||||
@ -47,6 +46,11 @@ export default definePlugin({
|
||||
default: false,
|
||||
onChange: v => toggleHoverControls(v)
|
||||
},
|
||||
useSpotifyUris: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Open Spotify URIs instead of Spotify URLs. Will only work if you have Spotify installed and might not work on all platforms",
|
||||
default: false
|
||||
}
|
||||
},
|
||||
patches: [
|
||||
{
|
||||
|
@ -5,6 +5,14 @@
|
||||
--vc-spotify-green: #1db954; /* so cusotm themes can easily change it */
|
||||
}
|
||||
|
||||
.theme-light #vc-spotify-player {
|
||||
background: var(--bg-overlay-3, var(--background-secondary-alt));
|
||||
}
|
||||
|
||||
.theme-dark #vc-spotify-player {
|
||||
background: var(--bg-overlay-1, var(--background-secondary-alt));
|
||||
}
|
||||
|
||||
.vc-spotify-button {
|
||||
background: none;
|
||||
color: var(--interactive-normal);
|
||||
@ -183,3 +191,8 @@
|
||||
.vc-spotify-time-right {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.vc-spotify-fallback {
|
||||
padding: 0.5em;
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ const REMEMBER_DISMISS_KEY = "Vencord-SupportHelper-Dismiss";
|
||||
export default definePlugin({
|
||||
name: "SupportHelper",
|
||||
required: true,
|
||||
description: "Helps me provide support to you",
|
||||
description: "Helps us provide support to you",
|
||||
authors: [Devs.Ven],
|
||||
|
||||
commands: [{
|
||||
|
@ -35,8 +35,6 @@ export default definePlugin({
|
||||
authors: [Devs.Ven],
|
||||
description: "Makes Avatars/Banners in user profiles clickable, and adds Guild Context Menu Entries to View Banner/Icon.",
|
||||
|
||||
dependencies: ["MenuItemDeobfuscatorAPI"],
|
||||
|
||||
openImage(url: string) {
|
||||
const u = new URL(url);
|
||||
u.searchParams.set("size", "512");
|
||||
|
110
src/plugins/wikisearch.ts
Normal file
110
src/plugins/wikisearch.ts
Normal file
@ -0,0 +1,110 @@
|
||||
/*
|
||||
* 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 { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
|
||||
export default definePlugin({
|
||||
name: "Wikisearch",
|
||||
description: "Searches Wikipedia for your requested query. (/wikisearch)",
|
||||
authors: [Devs.Samu],
|
||||
dependencies: ["CommandsAPI"],
|
||||
commands: [
|
||||
{
|
||||
name: "wikisearch",
|
||||
description: "Searches Wikipedia for your request.",
|
||||
inputType: ApplicationCommandInputType.BUILT_IN,
|
||||
options: [
|
||||
{
|
||||
name: "search",
|
||||
description: "Word to search for",
|
||||
type: ApplicationCommandOptionType.STRING,
|
||||
required: true
|
||||
},
|
||||
],
|
||||
execute: async (_, ctx) => {
|
||||
const word = findOption(_, "search", "");
|
||||
|
||||
if (!word) {
|
||||
return sendBotMessage(ctx.channel.id, {
|
||||
content: "No word was defined!"
|
||||
});
|
||||
}
|
||||
|
||||
const dataSearchParams = new URLSearchParams({
|
||||
action: "query",
|
||||
format: "json",
|
||||
list: "search",
|
||||
formatversion: "2",
|
||||
origin: "*",
|
||||
srsearch: word
|
||||
});
|
||||
|
||||
const data = await fetch("https://en.wikipedia.org/w/api.php?" + dataSearchParams).then(response => response.json())
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
sendBotMessage(ctx.channel.id, { content: "There was an error. Check the console for more info" });
|
||||
return null;
|
||||
});
|
||||
|
||||
if (!data) return;
|
||||
|
||||
if (!data.query?.search?.length) {
|
||||
console.log(data);
|
||||
return sendBotMessage(ctx.channel.id, { content: "No results given" });
|
||||
}
|
||||
|
||||
const altData = await fetch(`https://en.wikipedia.org/w/api.php?action=query&format=json&prop=info%7Cdescription%7Cimages%7Cimageinfo%7Cpageimages&list=&meta=&indexpageids=1&pageids=${data.query.search[0].pageid}&formatversion=2&origin=*`)
|
||||
.then(res => res.json())
|
||||
.then(data => data.query.pages[0])
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
sendBotMessage(ctx.channel.id, { content: "There was an error. Check the console for more info" });
|
||||
return null;
|
||||
});
|
||||
|
||||
if (!altData) return;
|
||||
|
||||
const thumbnailData = altData.thumbnail;
|
||||
|
||||
const thumbnail = thumbnailData && {
|
||||
url: thumbnailData.source.replace(/(50px-)/ig, "1000px-"),
|
||||
height: thumbnailData.height * 100,
|
||||
width: thumbnailData.width * 100
|
||||
};
|
||||
|
||||
sendBotMessage(ctx.channel.id, {
|
||||
embeds: [
|
||||
{
|
||||
type: "rich",
|
||||
title: data.query.search[0].title,
|
||||
url: `https://wikipedia.org/w/index.php?curid=${data.query.search[0].pageid}`,
|
||||
color: "0x8663BE",
|
||||
description: data.query.search[0].snippet.replace(/( |<([^>]+)>)/ig, "").replace(/(")/ig, "\"") + "...",
|
||||
image: thumbnail,
|
||||
footer: {
|
||||
text: "Powered by the Wikimedia API",
|
||||
},
|
||||
}
|
||||
] as any
|
||||
});
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
@ -54,7 +54,9 @@ if (location.protocol !== "data:") {
|
||||
document.getElementById("vencord-css-core")!.textContent = readFileSync(rendererCss, "utf-8");
|
||||
});
|
||||
}
|
||||
require(process.env.DISCORD_PRELOAD!);
|
||||
|
||||
if (process.env.DISCORD_PRELOAD)
|
||||
require(process.env.DISCORD_PRELOAD);
|
||||
} else {
|
||||
// Monaco Popout
|
||||
contextBridge.exposeInMainWorld("setCss", debounce(s => VencordNative.ipc.invoke(IpcEvents.SET_QUICK_CSS, s)));
|
||||
|
@ -194,6 +194,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
||||
name: "Captain",
|
||||
id: 347366054806159360n
|
||||
},
|
||||
nick: {
|
||||
name: "nick",
|
||||
id: 347884694408265729n
|
||||
},
|
||||
whqwert: {
|
||||
name: "whqwert",
|
||||
id: 586239091520176128n
|
||||
@ -222,6 +226,18 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
||||
name: "TheKodeToad",
|
||||
id: 706152404072267788n
|
||||
},
|
||||
juby: {
|
||||
name: "Juby210",
|
||||
id: 324622488644616195n
|
||||
},
|
||||
Alyxia: {
|
||||
name: "Alyxia Sother",
|
||||
id: 952185386350829688n
|
||||
},
|
||||
Remty: {
|
||||
name: "Remty",
|
||||
id: 335055032204656642n
|
||||
},
|
||||
skyevg: {
|
||||
name: "skyevg",
|
||||
id: 1090310844283363348n
|
||||
|
24
src/utils/native.ts
Normal file
24
src/utils/native.ts
Normal file
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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 function relaunch() {
|
||||
if (IS_DISCORD_DESKTOP)
|
||||
window.DiscordNative.app.relaunch();
|
||||
else
|
||||
window.VencordDesktop.app.relaunch();
|
||||
}
|
@ -47,7 +47,9 @@ export async function downloadSettingsBackup() {
|
||||
const backup = await exportSettings();
|
||||
const data = new TextEncoder().encode(backup);
|
||||
|
||||
if (IS_WEB) {
|
||||
if (IS_DISCORD_DESKTOP) {
|
||||
DiscordNative.fileManager.saveWithDialog(data, filename);
|
||||
} else {
|
||||
const file = new File([data], filename, { type: "application/json" });
|
||||
const a = document.createElement("a");
|
||||
a.href = URL.createObjectURL(file);
|
||||
@ -59,8 +61,6 @@ export async function downloadSettingsBackup() {
|
||||
URL.revokeObjectURL(a.href);
|
||||
document.body.removeChild(a);
|
||||
});
|
||||
} else {
|
||||
DiscordNative.fileManager.saveWithDialog(data, filename);
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,7 +77,24 @@ const toastFailure = (err: any) => Toasts.show({
|
||||
});
|
||||
|
||||
export async function uploadSettingsBackup(showToast = true): Promise<void> {
|
||||
if (IS_WEB) {
|
||||
if (IS_DISCORD_DESKTOP) {
|
||||
const [file] = await DiscordNative.fileManager.openFiles({
|
||||
filters: [
|
||||
{ name: "Vencord Settings Backup", extensions: ["json"] },
|
||||
{ name: "all", extensions: ["*"] }
|
||||
]
|
||||
});
|
||||
|
||||
if (file) {
|
||||
try {
|
||||
await importSettings(new TextDecoder().decode(file.data));
|
||||
if (showToast) toastSuccess();
|
||||
} catch (err) {
|
||||
new Logger("SettingsSync").error(err);
|
||||
if (showToast) toastFailure(err);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.style.display = "none";
|
||||
@ -102,22 +119,5 @@ export async function uploadSettingsBackup(showToast = true): Promise<void> {
|
||||
document.body.appendChild(input);
|
||||
input.click();
|
||||
setImmediate(() => document.body.removeChild(input));
|
||||
} else {
|
||||
const [file] = await DiscordNative.fileManager.openFiles({
|
||||
filters: [
|
||||
{ name: "Vencord Settings Backup", extensions: ["json"] },
|
||||
{ name: "all", extensions: ["*"] }
|
||||
]
|
||||
});
|
||||
|
||||
if (file) {
|
||||
try {
|
||||
await importSettings(new TextDecoder().decode(file.data));
|
||||
if (showToast) toastSuccess();
|
||||
} catch (err) {
|
||||
new Logger("SettingsSync").error(err);
|
||||
if (showToast) toastFailure(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import gitHash from "~git-hash";
|
||||
|
||||
import IpcEvents from "./IpcEvents";
|
||||
import Logger from "./Logger";
|
||||
import { relaunch } from "./native";
|
||||
import { IpcRes } from "./types";
|
||||
|
||||
export const UpdateLogger = /* #__PURE__*/ new Logger("Updater", "white");
|
||||
@ -90,8 +91,10 @@ export async function maybePromptToUpdate(confirmMessage: string, checkForDev =
|
||||
if (wantsUpdate) {
|
||||
await update();
|
||||
const needFullRestart = await rebuild();
|
||||
if (needFullRestart) DiscordNative.app.relaunch();
|
||||
else location.reload();
|
||||
if (needFullRestart)
|
||||
relaunch();
|
||||
else
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
|
@ -17,40 +17,37 @@
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line path-alias/no-relative
|
||||
import { filters, findByPropsLazy } from "../webpack";
|
||||
import { filters, findByPropsLazy, waitFor } from "@webpack";
|
||||
|
||||
import { waitForComponent } from "./internal";
|
||||
import * as t from "./types/components";
|
||||
|
||||
export const Forms = {
|
||||
FormTitle: waitForComponent<t.FormTitle>("FormTitle", filters.byCode("errorSeparator")),
|
||||
FormSection: waitForComponent<t.FormSection>("FormSection", filters.byCode("titleClassName", "sectionTitle")),
|
||||
FormDivider: waitForComponent<t.FormDivider>("FormDivider", m => {
|
||||
if (typeof m !== "function") return false;
|
||||
const s = m.toString();
|
||||
return s.length < 200 && s.includes(".divider");
|
||||
}),
|
||||
FormText: waitForComponent<t.FormText>("FormText", m => m.Types?.INPUT_PLACEHOLDER),
|
||||
export let Forms = {} as {
|
||||
FormTitle: t.FormTitle,
|
||||
FormSection: t.FormSection,
|
||||
FormDivider: t.FormDivider,
|
||||
FormText: t.FormText,
|
||||
};
|
||||
|
||||
export const Card = waitForComponent<t.Card>("Card", m => m.Types?.PRIMARY && m.defaultProps);
|
||||
export const Button = waitForComponent<t.Button>("Button", ["Hovers", "Looks", "Sizes"]);
|
||||
export const Switch = waitForComponent<t.Switch>("Switch", filters.byCode("tooltipNote", "ringTarget"));
|
||||
export const Tooltip = waitForComponent<t.Tooltip>("Tooltip", filters.byCode("shouldShowTooltip:!1", "clickableOnMobile||"));
|
||||
export let Card: t.Card;
|
||||
export let Button: t.Button;
|
||||
export let Switch: t.Switch;
|
||||
export let Tooltip: t.Tooltip;
|
||||
export let TextInput: t.TextInput;
|
||||
export let TextArea: t.TextArea;
|
||||
export let Text: t.Text;
|
||||
export let Select: t.Select;
|
||||
export let SearchableSelect: t.SearchableSelect;
|
||||
export let Slider: t.Slider;
|
||||
export let ButtonLooks: t.ButtonLooks;
|
||||
export let TabBar: any;
|
||||
|
||||
export const Timestamp = waitForComponent<t.Timestamp>("Timestamp", filters.byCode(".Messages.MESSAGE_EDITED_TIMESTAMP_A11Y_LABEL.format"));
|
||||
export const TextInput = waitForComponent<t.TextInput>("TextInput", ["defaultProps", "Sizes", "contextType"]);
|
||||
export const TextArea = waitForComponent<t.TextArea>("TextArea", filters.byCode("handleSetRef", "textArea"));
|
||||
export const Text = waitForComponent<t.Text>("Text", m => {
|
||||
if (typeof m !== "function") return false;
|
||||
const s = m.toString();
|
||||
return (s.length < 1500 && s.includes("data-text-variant") && s.includes("always-white"));
|
||||
});
|
||||
export const Select = waitForComponent<t.Select>("Select", filters.byCode("optionClassName", "popoutPosition", "autoFocus", "maxVisibleItems"));
|
||||
const searchableSelectFilter = filters.byCode("autoFocus", ".Messages.SELECT");
|
||||
export const SearchableSelect = waitForComponent<t.SearchableSelect>("SearchableSelect", m =>
|
||||
m.render && searchableSelectFilter(m.render)
|
||||
);
|
||||
export const Slider = waitForComponent<t.Slider>("Slider", filters.byCode("closestMarkerIndex", "stickToMarkers"));
|
||||
export const Flex = waitForComponent<t.Flex>("Flex", ["Justify", "Align", "Wrap"]);
|
||||
|
||||
export const ButtonWrapperClasses = findByPropsLazy("buttonWrapper", "buttonContent") as Record<string, string>;
|
||||
export const ButtonLooks: t.ButtonLooks = findByPropsLazy("BLANK", "FILLED", "INVERTED");
|
||||
|
||||
waitFor("FormItem", m => {
|
||||
({ Card, Button, FormSwitch: Switch, Tooltip, TextInput, TextArea, Text, Select, SearchableSelect, Slider, ButtonLooks, TabBar } = m);
|
||||
Forms = m;
|
||||
});
|
||||
|
@ -16,32 +16,14 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { proxyLazy } from "@utils/proxyLazy";
|
||||
|
||||
// eslint-disable-next-line path-alias/no-relative
|
||||
import { filters, mapMangledModule, mapMangledModuleLazy } from "../webpack";
|
||||
import { filters, mapMangledModuleLazy, waitFor } from "../webpack";
|
||||
import type * as t from "./types/menu";
|
||||
|
||||
export const Menu: t.Menu = proxyLazy(() => {
|
||||
const hasDeobfuscator = Vencord.Settings.plugins.MenuItemDeobfuscatorAPI.enabled;
|
||||
const menuItems = ["MenuSeparator", "MenuGroup", "MenuItem", "MenuCheckboxItem", "MenuRadioItem", "MenuControlItem"];
|
||||
export let Menu = {} as t.Menu;
|
||||
|
||||
const map = mapMangledModule("♫ ⊂(。◕‿‿◕。⊂) ♪", {
|
||||
ContextMenu: filters.byCode("getContainerProps"),
|
||||
...Object.fromEntries((hasDeobfuscator ? menuItems : []).map(s => [s, (m: any) => m.name === s]))
|
||||
}) as t.Menu;
|
||||
|
||||
if (!hasDeobfuscator) {
|
||||
for (const m of menuItems)
|
||||
Object.defineProperty(map, m, {
|
||||
get() {
|
||||
throw new Error("MenuItemDeobfuscator must be enabled to use this.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return map;
|
||||
});
|
||||
waitFor("MenuItem", m => Menu = m);
|
||||
|
||||
export const ContextMenu: t.ContextMenuApi = mapMangledModuleLazy('type:"CONTEXT_MENU_OPEN"', {
|
||||
open: filters.byCode("stopPropagation"),
|
||||
|
@ -24,10 +24,12 @@ export let useState: typeof React.useState;
|
||||
export let useEffect: typeof React.useEffect;
|
||||
export let useMemo: typeof React.useMemo;
|
||||
export let useRef: typeof React.useRef;
|
||||
export let useReducer: typeof React.useReducer;
|
||||
export let useCallback: typeof React.useCallback;
|
||||
|
||||
export const ReactDOM: typeof import("react-dom") & typeof import("react-dom/client") = findByPropsLazy("createPortal", "render");
|
||||
|
||||
waitFor("useState", m => {
|
||||
React = m;
|
||||
({ useEffect, useState, useMemo, useRef } = React);
|
||||
({ useEffect, useState, useMemo, useRef, useReducer, useCallback } = React);
|
||||
});
|
||||
|
10
src/webpack/common/types/menu.d.ts
vendored
10
src/webpack/common/types/menu.d.ts
vendored
@ -21,7 +21,7 @@ import type { ComponentType, CSSProperties, PropsWithChildren, UIEvent } from "r
|
||||
type RC<C> = ComponentType<PropsWithChildren<C & Record<string, any>>>;
|
||||
|
||||
export interface Menu {
|
||||
ContextMenu: RC<{
|
||||
Menu: RC<{
|
||||
navId: string;
|
||||
onClose(): void;
|
||||
className?: string;
|
||||
@ -49,19 +49,21 @@ export interface Menu {
|
||||
id: string;
|
||||
interactive?: boolean;
|
||||
}>;
|
||||
// TODO: Type me
|
||||
MenuSliderControl: RC<any>;
|
||||
}
|
||||
|
||||
export interface ContextMenuApi {
|
||||
close(): void;
|
||||
open(
|
||||
event: UIEvent,
|
||||
render?: Menu["ContextMenu"],
|
||||
render?: Menu["Menu"],
|
||||
options?: { enableSpellCheck?: boolean; },
|
||||
renderLazy?: () => Promise<Menu["ContextMenu"]>
|
||||
renderLazy?: () => Promise<Menu["Menu"]>
|
||||
): void;
|
||||
openLazy(
|
||||
event: UIEvent,
|
||||
renderLazy?: () => Promise<Menu["ContextMenu"]>,
|
||||
renderLazy?: () => Promise<Menu["Menu"]>,
|
||||
options?: { enableSpellCheck?: boolean; }
|
||||
): void;
|
||||
}
|
||||
|
1
src/webpack/common/types/utils.d.ts
vendored
1
src/webpack/common/types/utils.d.ts
vendored
@ -30,6 +30,7 @@ export interface FluxDispatcher {
|
||||
isDispatching(): boolean;
|
||||
subscribe(event: FluxEvents, callback: (data: any) => void): void;
|
||||
unsubscribe(event: FluxEvents, callback: (data: any) => void): void;
|
||||
wait(callback: () => void): void;
|
||||
}
|
||||
|
||||
export type Parser = Record<
|
||||
|
@ -67,7 +67,7 @@ export function _initWebpack(instance: typeof window.webpackChunkdiscord_app) {
|
||||
instance.pop();
|
||||
}
|
||||
|
||||
if (IS_DEV && !IS_WEB) {
|
||||
if (IS_DEV && IS_DISCORD_DESKTOP) {
|
||||
var devToolsOpen = false;
|
||||
// At this point in time, DiscordNative has not been exposed yet, so setImmediate is needed
|
||||
setTimeout(() => {
|
||||
@ -109,6 +109,8 @@ export const find = traceFunction("find", function find(filter: FilterFn, getDef
|
||||
if (!isWaitFor) {
|
||||
const err = new Error("Didn't find module matching this filter");
|
||||
if (IS_DEV) {
|
||||
logger.error(err);
|
||||
logger.error(filter);
|
||||
if (!devToolsOpen)
|
||||
// Strict behaviour in DevBuilds to fail early and make sure the issue is found
|
||||
throw err;
|
||||
|
Reference in New Issue
Block a user