Compare commits
86 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
6b10947d06 | ||
|
97b6699afe | ||
|
7e91edc757 | ||
|
a82544e93e | ||
|
d8c8b74ed7 | ||
|
fadd1598f5 | ||
|
e5c0898dd6 | ||
|
9550b74b2a | ||
|
6cfb67a52e | ||
|
12fe367385 | ||
|
fc0417eb80 | ||
|
1f87d14ab2 | ||
|
7d8f3508a8 | ||
|
41f5d71e38 | ||
|
efb88a4df8 | ||
|
a73d09a2f0 | ||
|
cf7c4d63b6 | ||
|
a95311ef2c | ||
|
dd23f9802c | ||
|
f23ddf4cae | ||
|
4222c7fd9f | ||
|
09f65b401e | ||
|
7364776715 | ||
|
3a5b70d410 | ||
|
e08d49edac | ||
|
17abbd3e3e | ||
|
25a1d934c6 | ||
|
620319a4bc | ||
|
4c9996d620 | ||
|
885c75fdaa | ||
|
f2a22c5e57 | ||
|
5e3a485edc | ||
|
452bf72e56 | ||
|
afa47addd7 | ||
|
8ecee3d09f | ||
|
d2aac5edc5 | ||
|
fc45510053 | ||
|
77bfaf38a5 | ||
|
a4cadc03d8 | ||
|
315fcf1972 | ||
|
f1b3b3c0a3 | ||
|
4c805d08be | ||
|
9b987d1e56 | ||
|
774318d583 | ||
|
7d954f9ade | ||
|
860d6edc7b | ||
|
223b0366c6 | ||
|
69cb7593eb | ||
|
8614e17633 | ||
|
aecd9d8fda | ||
|
faeb4fb585 | ||
|
0d18b44ba7 | ||
|
d671bd65ac | ||
|
8e952c630b | ||
|
2c758ccdf8 | ||
|
c165725297 | ||
|
29fbe3701a | ||
|
0b7c0e9587 | ||
|
d88524e8cf | ||
|
d6efd99849 | ||
|
fe6be987fd | ||
|
d688075c0a | ||
|
c752be45b2 | ||
|
07c1f5eed1 | ||
|
4df01b1e62 | ||
|
ebe10d3fad | ||
|
eca4af829f | ||
|
60458cdf1f | ||
|
714d87241c | ||
|
f628aa7a70 | ||
|
0f0282551d | ||
|
0335e1ca59 | ||
|
817f0f7473 | ||
|
98a03c8862 | ||
|
72ce7a5ad1 | ||
|
e699ea63c7 | ||
|
97e1e9eb7a | ||
|
4c4036546a | ||
|
d582e61ec3 | ||
|
ccd2ce8baf | ||
|
ede507e80c | ||
|
ffdf63563b | ||
|
55b755b2df | ||
|
ca439e9e9a | ||
|
e02fcce3dc | ||
|
3e732646e5 |
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@ -38,11 +38,11 @@ jobs:
|
|||||||
run: pnpm build --standalone
|
run: pnpm build --standalone
|
||||||
|
|
||||||
- name: Generate plugin list
|
- name: Generate plugin list
|
||||||
run: pnpm generatePluginJson dist/plugins.json
|
run: pnpm generatePluginJson dist/plugins.json dist/plugin-readmes.json
|
||||||
|
|
||||||
- name: Clean up obsolete files
|
- name: Clean up obsolete files
|
||||||
run: |
|
run: |
|
||||||
rm -rf dist/*-unpacked Vencord.user.css vencordDesktopRenderer.css vencordDesktopRenderer.css.map
|
rm -rf dist/*-unpacked dist/monaco Vencord.user.css vencordDesktopRenderer.css vencordDesktopRenderer.css.map
|
||||||
|
|
||||||
- name: Get some values needed for the release
|
- name: Get some values needed for the release
|
||||||
id: release_values
|
id: release_values
|
||||||
|
@ -65,7 +65,7 @@ Also pay attention to the following:
|
|||||||
- Match any but a guaranteed terminating character: `[^;]+`, for example to match the entire assigned value in `var a=b||c||func();`,
|
- Match any but a guaranteed terminating character: `[^;]+`, for example to match the entire assigned value in `var a=b||c||func();`,
|
||||||
`var .{1,2}=([^;]+);`
|
`var .{1,2}=([^;]+);`
|
||||||
- If you don't care about that part, just match a bunch of chars: `.{0,50}`, for example to extract the variable "b" in `createElement("div",{a:"foo",c:"bar"},b)`, `createElement\("div".{0,30},(.{1,2})\),`. Note the `.{0,30}`, this is essentially the same as `.+`, but safer as you can't end up accidently eating thousands of characters
|
- If you don't care about that part, just match a bunch of chars: `.{0,50}`, for example to extract the variable "b" in `createElement("div",{a:"foo",c:"bar"},b)`, `createElement\("div".{0,30},(.{1,2})\),`. Note the `.{0,30}`, this is essentially the same as `.+`, but safer as you can't end up accidently eating thousands of characters
|
||||||
- Additionally, as you might have noticed, all of the appove approaches use regex groups (`(...)`) to capture the variable name. You can then use those groups in your replacement to access those variables dynamically
|
- Additionally, as you might have noticed, all of the above approaches use regex groups (`(...)`) to capture the variable name. You can then use those groups in your replacement to access those variables dynamically
|
||||||
|
|
||||||
#### "replace"
|
#### "replace"
|
||||||
|
|
||||||
|
@ -4,8 +4,7 @@
|
|||||||
|
|
||||||
The cutest Discord client mod
|
The cutest Discord client mod
|
||||||
|
|
||||||
![](https://user-images.githubusercontent.com/45497981/235015332-0453d3eb-1da6-4601-963e-ef5e454123a1.png)
|
![image](https://github.com/Vendicated/Vencord/assets/45497981/706722b1-32de-4d99-bee9-93993b504334)
|
||||||
*A screenshot of Vencord featuring the [ClearVision-v6 theme](https://github.com/ClearVision/ClearVision-v6) (Vencord does not come with it pre-installed, it is only an example)*
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
@ -19,9 +19,11 @@
|
|||||||
/// <reference path="../src/modules.d.ts" />
|
/// <reference path="../src/modules.d.ts" />
|
||||||
/// <reference path="../src/globals.d.ts" />
|
/// <reference path="../src/globals.d.ts" />
|
||||||
|
|
||||||
import monacoHtml from "~fileContent/../src/components/monacoWin.html";
|
import monacoHtmlLocal from "~fileContent/monacoWin.html";
|
||||||
|
import monacoHtmlCdn from "~fileContent/../src/main/monacoWin.html";
|
||||||
import * as DataStore from "../src/api/DataStore";
|
import * as DataStore from "../src/api/DataStore";
|
||||||
import { debounce } from "../src/utils";
|
import { debounce } from "../src/utils";
|
||||||
|
import { EXTENSION_BASE_URL } from "../src/utils/web-metadata";
|
||||||
import { getTheme, Theme } from "../src/utils/discord";
|
import { getTheme, Theme } from "../src/utils/discord";
|
||||||
import { getThemeInfo } from "../src/main/themes";
|
import { getThemeInfo } from "../src/main/themes";
|
||||||
|
|
||||||
@ -80,6 +82,7 @@ window.VencordNative = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
win.baseUrl = EXTENSION_BASE_URL;
|
||||||
win.setCss = setCssDebounced;
|
win.setCss = setCssDebounced;
|
||||||
win.getCurrentCss = () => VencordNative.quickCss.get();
|
win.getCurrentCss = () => VencordNative.quickCss.get();
|
||||||
win.getTheme = () =>
|
win.getTheme = () =>
|
||||||
@ -87,7 +90,7 @@ window.VencordNative = {
|
|||||||
? "vs-light"
|
? "vs-light"
|
||||||
: "vs-dark";
|
: "vs-dark";
|
||||||
|
|
||||||
win.document.write(monacoHtml);
|
win.document.write(IS_EXTENSION ? monacoHtmlLocal : monacoHtmlCdn);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
/**
|
|
||||||
* @template T
|
|
||||||
* @param {T[]} arr
|
|
||||||
* @param {(v: T) => boolean} predicate
|
|
||||||
*/
|
|
||||||
function removeFirst(arr, predicate) {
|
|
||||||
const idx = arr.findIndex(predicate);
|
|
||||||
if (idx !== -1) arr.splice(idx, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
chrome.webRequest.onHeadersReceived.addListener(
|
|
||||||
({ responseHeaders, type, url }) => {
|
|
||||||
if (!responseHeaders) return;
|
|
||||||
|
|
||||||
if (type === "main_frame") {
|
|
||||||
// In main frame requests, the CSP needs to be removed to enable fetching of custom css
|
|
||||||
// as desired by the user
|
|
||||||
removeFirst(responseHeaders, h => h.name.toLowerCase() === "content-security-policy");
|
|
||||||
} else if (type === "stylesheet" && url.startsWith("https://raw.githubusercontent.com/")) {
|
|
||||||
// Most users will load css from GitHub, but GitHub doesn't set the correct content type,
|
|
||||||
// so we fix it here
|
|
||||||
removeFirst(responseHeaders, h => h.name.toLowerCase() === "content-type");
|
|
||||||
responseHeaders.push({
|
|
||||||
name: "Content-Type",
|
|
||||||
value: "text/css"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return { responseHeaders };
|
|
||||||
},
|
|
||||||
{ urls: ["https://raw.githubusercontent.com/*", "*://*.discord.com/*"], types: ["main_frame", "stylesheet"] },
|
|
||||||
["blocking", "responseHeaders"]
|
|
||||||
);
|
|
@ -4,6 +4,11 @@ if (typeof browser === "undefined") {
|
|||||||
|
|
||||||
const script = document.createElement("script");
|
const script = document.createElement("script");
|
||||||
script.src = browser.runtime.getURL("dist/Vencord.js");
|
script.src = browser.runtime.getURL("dist/Vencord.js");
|
||||||
|
script.id = "vencord-script";
|
||||||
|
Object.assign(script.dataset, {
|
||||||
|
extensionBaseUrl: browser.runtime.getURL(""),
|
||||||
|
version: browser.runtime.getManifest().version
|
||||||
|
});
|
||||||
|
|
||||||
const style = document.createElement("link");
|
const style = document.createElement("link");
|
||||||
style.type = "text/css";
|
style.type = "text/css";
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
"web_accessible_resources": [
|
"web_accessible_resources": [
|
||||||
{
|
{
|
||||||
"resources": ["dist/Vencord.js", "dist/Vencord.css"],
|
"resources": ["dist/*", "third-party/*"],
|
||||||
"matches": ["*://*.discord.com/*"]
|
"matches": ["*://*.discord.com/*"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -26,11 +26,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
"background": {
|
"web_accessible_resources": ["dist/*", "third-party/*"],
|
||||||
"scripts": ["background.js"]
|
|
||||||
},
|
|
||||||
|
|
||||||
"web_accessible_resources": ["dist/Vencord.js", "dist/Vencord.css"],
|
|
||||||
|
|
||||||
"browser_specific_settings": {
|
"browser_specific_settings": {
|
||||||
"gecko": {
|
"gecko": {
|
||||||
|
43
browser/monaco.ts
Normal file
43
browser/monaco.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "./patch-worker";
|
||||||
|
|
||||||
|
import * as monaco from "monaco-editor/esm/vs/editor/editor.main.js";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
const baseUrl: string;
|
||||||
|
const getCurrentCss: () => Promise<string>;
|
||||||
|
const setCss: (css: string) => void;
|
||||||
|
const getTheme: () => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BASE = "/dist/monaco/vs";
|
||||||
|
|
||||||
|
self.MonacoEnvironment = {
|
||||||
|
getWorkerUrl(_moduleId: unknown, label: string) {
|
||||||
|
const path = label === "css" ? "/language/css/css.worker.js" : "/editor/editor.worker.js";
|
||||||
|
return new URL(BASE + path, baseUrl).toString();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
getCurrentCss().then(css => {
|
||||||
|
const editor = monaco.editor.create(
|
||||||
|
document.getElementById("container")!,
|
||||||
|
{
|
||||||
|
value: css,
|
||||||
|
language: "css",
|
||||||
|
theme: getTheme(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
editor.onDidChangeModelContent(() =>
|
||||||
|
setCss(editor.getValue())
|
||||||
|
);
|
||||||
|
window.addEventListener("resize", () => {
|
||||||
|
// make monaco re-layout
|
||||||
|
editor.layout();
|
||||||
|
});
|
||||||
|
});
|
37
browser/monacoWin.html
Normal file
37
browser/monacoWin.html
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Vencord QuickCSS Editor</title>
|
||||||
|
<style>
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#container {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="container"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const script = document.createElement("script");
|
||||||
|
script.src = new URL("/dist/monaco/index.js", baseUrl);
|
||||||
|
|
||||||
|
const style = document.createElement("link");
|
||||||
|
style.type = "text/css";
|
||||||
|
style.rel = "stylesheet";
|
||||||
|
style.href = new URL("/dist/monaco/index.css", baseUrl);
|
||||||
|
|
||||||
|
document.body.append(style, script);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
135
browser/patch-worker.js
Normal file
135
browser/patch-worker.js
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2013 Rob Wu <gwnRob@gmail.com>
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
// Target: Chrome 20+
|
||||||
|
|
||||||
|
// W3-compliant Worker proxy.
|
||||||
|
// This module replaces the global Worker object.
|
||||||
|
// When invoked, the default Worker object is called.
|
||||||
|
// If this call fails with SECURITY_ERR, the script is fetched
|
||||||
|
// using async XHR, and transparently proxies all calls and
|
||||||
|
// setters/getters to the new Worker object.
|
||||||
|
// Note: This script does not magically circumvent the Same origin policy.
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
var Worker_ = window.Worker;
|
||||||
|
var URL = window.URL || window.webkitURL;
|
||||||
|
// Create dummy worker for the following purposes:
|
||||||
|
// 1. Don't override the global Worker object if the fallback isn't
|
||||||
|
// going to work (future API changes?)
|
||||||
|
// 2. Use it to trigger early validation of postMessage calls
|
||||||
|
// Note: Blob constructor is supported since Chrome 20, but since
|
||||||
|
// some of the used Chrome APIs are only supported as of Chrome 20,
|
||||||
|
// I don't bother adding a BlobBuilder fallback.
|
||||||
|
var dummyWorker = new Worker_(
|
||||||
|
URL.createObjectURL(new Blob([], { type: 'text/javascript' })));
|
||||||
|
window.Worker = function Worker(scriptURL) {
|
||||||
|
if (arguments.length === 0) {
|
||||||
|
throw new TypeError('Not enough arguments');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new Worker_(scriptURL);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.code === 18/*DOMException.SECURITY_ERR*/) {
|
||||||
|
return new WorkerXHR(scriptURL);
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Bind events and replay queued messages
|
||||||
|
function bindWorker(worker, workerURL) {
|
||||||
|
if (worker._terminated) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
worker.Worker = new Worker_(workerURL);
|
||||||
|
worker.Worker.onerror = worker._onerror;
|
||||||
|
worker.Worker.onmessage = worker._onmessage;
|
||||||
|
var o;
|
||||||
|
while ((o = worker._replayQueue.shift())) {
|
||||||
|
worker.Worker[o.method].apply(worker.Worker, o.arguments);
|
||||||
|
}
|
||||||
|
while ((o = worker._messageQueue.shift())) {
|
||||||
|
worker.Worker.postMessage.apply(worker.Worker, o);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function WorkerXHR(scriptURL) {
|
||||||
|
var worker = this;
|
||||||
|
var x = new XMLHttpRequest();
|
||||||
|
x.responseType = 'blob';
|
||||||
|
x.onload = function () {
|
||||||
|
// http://stackoverflow.com/a/10372280/938089
|
||||||
|
var workerURL = URL.createObjectURL(x.response);
|
||||||
|
bindWorker(worker, workerURL);
|
||||||
|
};
|
||||||
|
x.open('GET', scriptURL);
|
||||||
|
x.send();
|
||||||
|
worker._replayQueue = [];
|
||||||
|
worker._messageQueue = [];
|
||||||
|
}
|
||||||
|
WorkerXHR.prototype = {
|
||||||
|
constructor: Worker_,
|
||||||
|
terminate: function () {
|
||||||
|
if (!this._terminated) {
|
||||||
|
this._terminated = true;
|
||||||
|
if (this.Worker)
|
||||||
|
this.Worker.terminate();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
postMessage: function (message, transfer) {
|
||||||
|
if (!(this instanceof WorkerXHR))
|
||||||
|
throw new TypeError('Illegal invocation');
|
||||||
|
if (this.Worker) {
|
||||||
|
this.Worker.postMessage.apply(this.Worker, arguments);
|
||||||
|
} else {
|
||||||
|
// Trigger validation:
|
||||||
|
dummyWorker.postMessage(message);
|
||||||
|
// Alright, push the valid message to the queue.
|
||||||
|
this._messageQueue.push(arguments);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Implement the EventTarget interface
|
||||||
|
[
|
||||||
|
'addEventListener',
|
||||||
|
'removeEventListener',
|
||||||
|
'dispatchEvent'
|
||||||
|
].forEach(function (method) {
|
||||||
|
WorkerXHR.prototype[method] = function () {
|
||||||
|
if (!(this instanceof WorkerXHR)) {
|
||||||
|
throw new TypeError('Illegal invocation');
|
||||||
|
}
|
||||||
|
if (this.Worker) {
|
||||||
|
this.Worker[method].apply(this.Worker, arguments);
|
||||||
|
} else {
|
||||||
|
this._replayQueue.push({ method: method, arguments: arguments });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
Object.defineProperties(WorkerXHR.prototype, {
|
||||||
|
onmessage: {
|
||||||
|
get: function () { return this._onmessage || null; },
|
||||||
|
set: function (func) {
|
||||||
|
this._onmessage = typeof func === 'function' ? func : null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onerror: {
|
||||||
|
get: function () { return this._onerror || null; },
|
||||||
|
set: function (func) {
|
||||||
|
this._onerror = typeof func === 'function' ? func : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
@ -63,7 +63,7 @@ Then fully close Discord from your taskbar or task manager, and restart it. Venc
|
|||||||
|
|
||||||
If you're using Discord already, go into the `Updater` tab in settings.
|
If you're using Discord already, go into the `Updater` tab in settings.
|
||||||
|
|
||||||
Sometimes it may be neccessary to manually update if the GUI updater fails.
|
Sometimes it may be necessary to manually update if the GUI updater fails.
|
||||||
|
|
||||||
To pull latest changes:
|
To pull latest changes:
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "vencord",
|
"name": "vencord",
|
||||||
"private": "true",
|
"private": "true",
|
||||||
"version": "1.4.3",
|
"version": "1.5.1",
|
||||||
"description": "The cutest Discord client mod",
|
"description": "The cutest Discord client mod",
|
||||||
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
@ -31,14 +31,18 @@
|
|||||||
"watch": "node scripts/build/build.mjs --watch"
|
"watch": "node scripts/build/build.mjs --watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@sapphi-red/web-noise-suppressor": "0.3.3",
|
||||||
"@vap/core": "0.0.12",
|
"@vap/core": "0.0.12",
|
||||||
"@vap/shiki": "0.10.5",
|
"@vap/shiki": "0.10.5",
|
||||||
"eslint-plugin-simple-header": "^1.0.2",
|
"eslint-plugin-simple-header": "^1.0.2",
|
||||||
"fflate": "^0.7.4",
|
"fflate": "^0.7.4",
|
||||||
|
"gifenc": "github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3",
|
||||||
|
"monaco-editor": "^0.43.0",
|
||||||
"nanoid": "^4.0.2",
|
"nanoid": "^4.0.2",
|
||||||
"virtual-merge": "^1.0.1"
|
"virtual-merge": "^1.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/chrome": "^0.0.246",
|
||||||
"@types/diff": "^5.0.3",
|
"@types/diff": "^5.0.3",
|
||||||
"@types/lodash": "^4.14.194",
|
"@types/lodash": "^4.14.194",
|
||||||
"@types/node": "^18.16.3",
|
"@types/node": "^18.16.3",
|
||||||
@ -63,7 +67,8 @@
|
|||||||
"stylelint-config-standard": "^33.0.0",
|
"stylelint-config-standard": "^33.0.0",
|
||||||
"tsx": "^3.12.7",
|
"tsx": "^3.12.7",
|
||||||
"type-fest": "^3.9.0",
|
"type-fest": "^3.9.0",
|
||||||
"typescript": "^5.0.4"
|
"typescript": "^5.0.4",
|
||||||
|
"zip-local": "^0.3.5"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@8.1.1",
|
"packageManager": "pnpm@8.1.1",
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
|
88
pnpm-lock.yaml
generated
88
pnpm-lock.yaml
generated
@ -1,5 +1,9 @@
|
|||||||
lockfileVersion: '6.0'
|
lockfileVersion: '6.0'
|
||||||
|
|
||||||
|
settings:
|
||||||
|
autoInstallPeers: true
|
||||||
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
patchedDependencies:
|
patchedDependencies:
|
||||||
eslint-plugin-path-alias@1.0.0:
|
eslint-plugin-path-alias@1.0.0:
|
||||||
hash: m6sma4g6bh67km3q6igf6uxaja
|
hash: m6sma4g6bh67km3q6igf6uxaja
|
||||||
@ -9,6 +13,9 @@ patchedDependencies:
|
|||||||
path: patches/eslint@8.46.0.patch
|
path: patches/eslint@8.46.0.patch
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@sapphi-red/web-noise-suppressor':
|
||||||
|
specifier: 0.3.3
|
||||||
|
version: 0.3.3
|
||||||
'@vap/core':
|
'@vap/core':
|
||||||
specifier: 0.0.12
|
specifier: 0.0.12
|
||||||
version: 0.0.12
|
version: 0.0.12
|
||||||
@ -21,6 +28,12 @@ dependencies:
|
|||||||
fflate:
|
fflate:
|
||||||
specifier: ^0.7.4
|
specifier: ^0.7.4
|
||||||
version: 0.7.4
|
version: 0.7.4
|
||||||
|
gifenc:
|
||||||
|
specifier: github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3
|
||||||
|
version: github.com/mattdesl/gifenc/64842fca317b112a8590f8fef2bf3825da8f6fe3
|
||||||
|
monaco-editor:
|
||||||
|
specifier: ^0.43.0
|
||||||
|
version: 0.43.0
|
||||||
nanoid:
|
nanoid:
|
||||||
specifier: ^4.0.2
|
specifier: ^4.0.2
|
||||||
version: 4.0.2
|
version: 4.0.2
|
||||||
@ -29,6 +42,9 @@ dependencies:
|
|||||||
version: 1.0.1
|
version: 1.0.1
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@types/chrome':
|
||||||
|
specifier: ^0.0.246
|
||||||
|
version: 0.0.246
|
||||||
'@types/diff':
|
'@types/diff':
|
||||||
specifier: ^5.0.3
|
specifier: ^5.0.3
|
||||||
version: 5.0.3
|
version: 5.0.3
|
||||||
@ -104,6 +120,9 @@ devDependencies:
|
|||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.0.4
|
specifier: ^5.0.4
|
||||||
version: 5.0.4
|
version: 5.0.4
|
||||||
|
zip-local:
|
||||||
|
specifier: ^0.3.5
|
||||||
|
version: 0.3.5
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@ -513,10 +532,35 @@ packages:
|
|||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@sapphi-red/web-noise-suppressor@0.3.3:
|
||||||
|
resolution: {integrity: sha512-gAC33DCXYwNTI/k1PxOVHmbbzakUSMbb/DHpoV6rn4pKZtPI1dduULSmAAm/y1ipgIlArnk2JcnQzw4n2tCZHw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@types/chrome@0.0.246:
|
||||||
|
resolution: {integrity: sha512-MxGxEomGxsJiL9xe/7ZwVgwdn8XVKWbPvxpVQl3nWOjrS0Ce63JsfzxUc4aU3GvRcUPYsfufHmJ17BFyKxeA4g==}
|
||||||
|
dependencies:
|
||||||
|
'@types/filesystem': 0.0.33
|
||||||
|
'@types/har-format': 1.2.13
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/diff@5.0.3:
|
/@types/diff@5.0.3:
|
||||||
resolution: {integrity: sha512-amrLbRqTU9bXMCc6uX0sWpxsQzRIo9z6MJPkH1pkez/qOxuqSZVuryJAWoBRq94CeG8JxY+VK4Le9HtjQR5T9A==}
|
resolution: {integrity: sha512-amrLbRqTU9bXMCc6uX0sWpxsQzRIo9z6MJPkH1pkez/qOxuqSZVuryJAWoBRq94CeG8JxY+VK4Le9HtjQR5T9A==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/filesystem@0.0.33:
|
||||||
|
resolution: {integrity: sha512-2KedRPzwu2K528vFkoXnnWdsG0MtUwPjuA7pRy4vKxlxHEe8qUDZibYHXJKZZr2Cl/ELdCWYqyb/MKwsUuzBWw==}
|
||||||
|
dependencies:
|
||||||
|
'@types/filewriter': 0.0.30
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/filewriter@0.0.30:
|
||||||
|
resolution: {integrity: sha512-lB98tui0uxc7erbj0serZfJlHKLNJHwBltPnbmO1WRpL5T325GOHRiQfr2E29V2q+S1brDO63Fpdt6vb3bES9Q==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/har-format@1.2.13:
|
||||||
|
resolution: {integrity: sha512-PwBsCBD3lDODn4xpje3Y1di0aDJp4Ww7aSfMRVw6ysnxD4I7Wmq2mBkSKaDtN403hqH5sp6c9xQUvFYY3+lkBg==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/json-schema@7.0.11:
|
/@types/json-schema@7.0.11:
|
||||||
resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==}
|
resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -836,6 +880,10 @@ packages:
|
|||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/async@1.5.2:
|
||||||
|
resolution: {integrity: sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/atob@2.1.2:
|
/atob@2.1.2:
|
||||||
resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==}
|
resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==}
|
||||||
engines: {node: '>= 4.5.0'}
|
engines: {node: '>= 4.5.0'}
|
||||||
@ -1860,6 +1908,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==}
|
resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/graceful-fs@4.2.11:
|
||||||
|
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/grapheme-splitter@1.0.4:
|
/grapheme-splitter@1.0.4:
|
||||||
resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==}
|
resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -2177,6 +2229,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
|
resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/jszip@2.7.0:
|
||||||
|
resolution: {integrity: sha512-JIsRKRVC3gTRo2vM4Wy9WBC3TRcfnIZU8k65Phi3izkvPH975FowRYtKGT6PxevA0XnJ/yO8b0QwV0ydVyQwfw==}
|
||||||
|
dependencies:
|
||||||
|
pako: 1.0.11
|
||||||
|
dev: true
|
||||||
|
|
||||||
/kind-of@3.2.2:
|
/kind-of@3.2.2:
|
||||||
resolution: {integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==}
|
resolution: {integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@ -2347,6 +2405,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==}
|
resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/monaco-editor@0.43.0:
|
||||||
|
resolution: {integrity: sha512-cnoqwQi/9fml2Szamv1XbSJieGJ1Dc8tENVMD26Kcfl7xGQWp7OBKMjlwKVGYFJ3/AXJjSOGvcqK7Ry/j9BM1Q==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/ms@2.0.0:
|
/ms@2.0.0:
|
||||||
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -2504,6 +2566,10 @@ packages:
|
|||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/pako@1.0.11:
|
||||||
|
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/parent-module@1.0.1:
|
/parent-module@1.0.1:
|
||||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@ -2655,6 +2721,11 @@ packages:
|
|||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/q@1.5.1:
|
||||||
|
resolution: {integrity: sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==}
|
||||||
|
engines: {node: '>=0.6.0', teleport: '>=0.2.0'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/queue-microtask@1.2.3:
|
/queue-microtask@1.2.3:
|
||||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -3370,6 +3441,17 @@ packages:
|
|||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
settings:
|
/zip-local@0.3.5:
|
||||||
autoInstallPeers: true
|
resolution: {integrity: sha512-GRV3D5TJY+/PqyeRm5CYBs7xVrKTKzljBoEXvocZu0HJ7tPEcgpSOYa2zFIsCZWgKWMuc4U3yMFgFkERGFIB9w==}
|
||||||
excludeLinksFromLockfile: false
|
dependencies:
|
||||||
|
async: 1.5.2
|
||||||
|
graceful-fs: 4.2.11
|
||||||
|
jszip: 2.7.0
|
||||||
|
q: 1.5.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
github.com/mattdesl/gifenc/64842fca317b112a8590f8fef2bf3825da8f6fe3:
|
||||||
|
resolution: {tarball: https://codeload.github.com/mattdesl/gifenc/tar.gz/64842fca317b112a8590f8fef2bf3825da8f6fe3}
|
||||||
|
name: gifenc
|
||||||
|
version: 1.0.3
|
||||||
|
dev: false
|
||||||
|
@ -19,13 +19,14 @@
|
|||||||
|
|
||||||
import esbuild from "esbuild";
|
import esbuild from "esbuild";
|
||||||
|
|
||||||
import { commonOpts, globPlugins, isStandalone, VERSION, watch } from "./common.mjs";
|
import { BUILD_TIMESTAMP, commonOpts, globPlugins, isStandalone, updaterDisabled, VERSION, watch } from "./common.mjs";
|
||||||
|
|
||||||
const defines = {
|
const defines = {
|
||||||
IS_STANDALONE: isStandalone,
|
IS_STANDALONE: isStandalone,
|
||||||
IS_DEV: JSON.stringify(watch),
|
IS_DEV: JSON.stringify(watch),
|
||||||
|
IS_UPDATER_DISABLED: updaterDisabled,
|
||||||
VERSION: JSON.stringify(VERSION),
|
VERSION: JSON.stringify(VERSION),
|
||||||
BUILD_TIMESTAMP: Date.now(),
|
BUILD_TIMESTAMP,
|
||||||
};
|
};
|
||||||
if (defines.IS_STANDALONE === "false")
|
if (defines.IS_STANDALONE === "false")
|
||||||
// If this is a local build (not standalone), optimise
|
// If this is a local build (not standalone), optimise
|
||||||
@ -58,7 +59,7 @@ await Promise.all([
|
|||||||
define: {
|
define: {
|
||||||
...defines,
|
...defines,
|
||||||
IS_DISCORD_DESKTOP: true,
|
IS_DISCORD_DESKTOP: true,
|
||||||
IS_VENCORD_DESKTOP: false
|
IS_VESKTOP: false
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
esbuild.build({
|
esbuild.build({
|
||||||
@ -77,8 +78,9 @@ await Promise.all([
|
|||||||
define: {
|
define: {
|
||||||
...defines,
|
...defines,
|
||||||
IS_WEB: false,
|
IS_WEB: false,
|
||||||
|
IS_EXTENSION: false,
|
||||||
IS_DISCORD_DESKTOP: true,
|
IS_DISCORD_DESKTOP: true,
|
||||||
IS_VENCORD_DESKTOP: false
|
IS_VESKTOP: false
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
esbuild.build({
|
esbuild.build({
|
||||||
@ -90,7 +92,7 @@ await Promise.all([
|
|||||||
define: {
|
define: {
|
||||||
...defines,
|
...defines,
|
||||||
IS_DISCORD_DESKTOP: true,
|
IS_DISCORD_DESKTOP: true,
|
||||||
IS_VENCORD_DESKTOP: false
|
IS_VESKTOP: false
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@ -104,7 +106,7 @@ await Promise.all([
|
|||||||
define: {
|
define: {
|
||||||
...defines,
|
...defines,
|
||||||
IS_DISCORD_DESKTOP: false,
|
IS_DISCORD_DESKTOP: false,
|
||||||
IS_VENCORD_DESKTOP: true
|
IS_VESKTOP: true
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
esbuild.build({
|
esbuild.build({
|
||||||
@ -123,8 +125,9 @@ await Promise.all([
|
|||||||
define: {
|
define: {
|
||||||
...defines,
|
...defines,
|
||||||
IS_WEB: false,
|
IS_WEB: false,
|
||||||
|
IS_EXTENSION: false,
|
||||||
IS_DISCORD_DESKTOP: false,
|
IS_DISCORD_DESKTOP: false,
|
||||||
IS_VENCORD_DESKTOP: true
|
IS_VESKTOP: true
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
esbuild.build({
|
esbuild.build({
|
||||||
@ -136,7 +139,7 @@ await Promise.all([
|
|||||||
define: {
|
define: {
|
||||||
...defines,
|
...defines,
|
||||||
IS_DISCORD_DESKTOP: false,
|
IS_DISCORD_DESKTOP: false,
|
||||||
IS_VENCORD_DESKTOP: true
|
IS_VESKTOP: true
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
]).catch(err => {
|
]).catch(err => {
|
||||||
|
@ -17,14 +17,13 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import esbuild from "esbuild";
|
import esbuild from "esbuild";
|
||||||
import { zip } from "fflate";
|
|
||||||
import { readFileSync } from "fs";
|
import { readFileSync } from "fs";
|
||||||
import { appendFile, mkdir, readFile, rm, writeFile } from "fs/promises";
|
import { appendFile, mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
|
import Zip from "zip-local";
|
||||||
|
|
||||||
import { commonOpts, globPlugins, VERSION, watch } from "./common.mjs";
|
import { BUILD_TIMESTAMP, commonOpts, globPlugins, VERSION, watch } from "./common.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {esbuild.BuildOptions}
|
* @type {esbuild.BuildOptions}
|
||||||
@ -42,28 +41,69 @@ const commonOptions = {
|
|||||||
target: ["esnext"],
|
target: ["esnext"],
|
||||||
define: {
|
define: {
|
||||||
IS_WEB: "true",
|
IS_WEB: "true",
|
||||||
|
IS_EXTENSION: "false",
|
||||||
IS_STANDALONE: "true",
|
IS_STANDALONE: "true",
|
||||||
IS_DEV: JSON.stringify(watch),
|
IS_DEV: JSON.stringify(watch),
|
||||||
IS_DISCORD_DESKTOP: "false",
|
IS_DISCORD_DESKTOP: "false",
|
||||||
IS_VENCORD_DESKTOP: "false",
|
IS_VESKTOP: "false",
|
||||||
|
IS_UPDATER_DISABLED: "true",
|
||||||
VERSION: JSON.stringify(VERSION),
|
VERSION: JSON.stringify(VERSION),
|
||||||
BUILD_TIMESTAMP: Date.now(),
|
BUILD_TIMESTAMP,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MonacoWorkerEntryPoints = [
|
||||||
|
"vs/language/css/css.worker.js",
|
||||||
|
"vs/editor/editor.worker.js"
|
||||||
|
];
|
||||||
|
|
||||||
|
const RnNoiseFiles = [
|
||||||
|
"dist/rnnoise.wasm",
|
||||||
|
"dist/rnnoise_simd.wasm",
|
||||||
|
"dist/rnnoise/workletProcessor.js",
|
||||||
|
"LICENSE"
|
||||||
|
];
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
[
|
[
|
||||||
|
esbuild.build({
|
||||||
|
entryPoints: MonacoWorkerEntryPoints.map(entry => `node_modules/monaco-editor/esm/${entry}`),
|
||||||
|
bundle: true,
|
||||||
|
minify: true,
|
||||||
|
format: "iife",
|
||||||
|
outbase: "node_modules/monaco-editor/esm/",
|
||||||
|
outdir: "dist/monaco"
|
||||||
|
}),
|
||||||
|
esbuild.build({
|
||||||
|
entryPoints: ["browser/monaco.ts"],
|
||||||
|
bundle: true,
|
||||||
|
minify: true,
|
||||||
|
format: "iife",
|
||||||
|
outfile: "dist/monaco/index.js",
|
||||||
|
loader: {
|
||||||
|
".ttf": "file"
|
||||||
|
}
|
||||||
|
}),
|
||||||
esbuild.build({
|
esbuild.build({
|
||||||
...commonOptions,
|
...commonOptions,
|
||||||
outfile: "dist/browser.js",
|
outfile: "dist/browser.js",
|
||||||
footer: { js: "//# sourceURL=VencordWeb" },
|
footer: { js: "//# sourceURL=VencordWeb" },
|
||||||
}),
|
}),
|
||||||
|
esbuild.build({
|
||||||
|
...commonOptions,
|
||||||
|
outfile: "dist/extension.js",
|
||||||
|
define: {
|
||||||
|
...commonOptions?.define,
|
||||||
|
IS_EXTENSION: "true",
|
||||||
|
},
|
||||||
|
footer: { js: "//# sourceURL=VencordWeb" },
|
||||||
|
}),
|
||||||
esbuild.build({
|
esbuild.build({
|
||||||
...commonOptions,
|
...commonOptions,
|
||||||
inject: ["browser/GMPolyfill.js", ...(commonOptions?.inject || [])],
|
inject: ["browser/GMPolyfill.js", ...(commonOptions?.inject || [])],
|
||||||
define: {
|
define: {
|
||||||
"window": "unsafeWindow",
|
...(commonOptions?.define),
|
||||||
...(commonOptions?.define)
|
window: "unsafeWindow",
|
||||||
},
|
},
|
||||||
outfile: "dist/Vencord.user.js",
|
outfile: "dist/Vencord.user.js",
|
||||||
banner: {
|
banner: {
|
||||||
@ -78,12 +118,41 @@ await Promise.all(
|
|||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {(target: string, files: string[], shouldZip: boolean) => Promise<void>}
|
* @type {(dir: string) => Promise<string[]>}
|
||||||
*/
|
*/
|
||||||
async function buildPluginZip(target, files, shouldZip) {
|
async function globDir(dir) {
|
||||||
|
const files = [];
|
||||||
|
|
||||||
|
for (const child of await readdir(dir, { withFileTypes: true })) {
|
||||||
|
const p = join(dir, child.name);
|
||||||
|
if (child.isDirectory())
|
||||||
|
files.push(...await globDir(p));
|
||||||
|
else
|
||||||
|
files.push(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {(dir: string, basePath?: string) => Promise<Record<string, string>>}
|
||||||
|
*/
|
||||||
|
async function loadDir(dir, basePath = "") {
|
||||||
|
const files = await globDir(dir);
|
||||||
|
return Object.fromEntries(await Promise.all(files.map(async f => [f.slice(basePath.length), await readFile(f)])));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {(target: string, files: string[]) => Promise<void>}
|
||||||
|
*/
|
||||||
|
async function buildExtension(target, files, noMonaco = false) {
|
||||||
const entries = {
|
const entries = {
|
||||||
"dist/Vencord.js": await readFile("dist/browser.js"),
|
"dist/Vencord.js": await readFile("dist/extension.js"),
|
||||||
"dist/Vencord.css": await readFile("dist/browser.css"),
|
"dist/Vencord.css": await readFile("dist/extension.css"),
|
||||||
|
...(noMonaco ? {} : await loadDir("dist/monaco")),
|
||||||
|
...Object.fromEntries(await Promise.all(RnNoiseFiles.map(async file =>
|
||||||
|
[`third-party/rnnoise/${file.replace(/^dist\//, "")}`, await readFile(`node_modules/@sapphi-red/web-noise-suppressor/${file}`)]
|
||||||
|
))),
|
||||||
...Object.fromEntries(await Promise.all(files.map(async f => {
|
...Object.fromEntries(await Promise.all(files.map(async f => {
|
||||||
let content = await readFile(join("browser", f));
|
let content = await readFile(join("browser", f));
|
||||||
if (f.startsWith("manifest")) {
|
if (f.startsWith("manifest")) {
|
||||||
@ -99,21 +168,6 @@ async function buildPluginZip(target, files, shouldZip) {
|
|||||||
}))),
|
}))),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (shouldZip) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
zip(entries, {}, (err, data) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
} else {
|
|
||||||
const out = join("dist", target);
|
|
||||||
writeFile(out, data).then(() => {
|
|
||||||
console.info("Extension written to " + out);
|
|
||||||
resolve();
|
|
||||||
}).catch(reject);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await rm(target, { recursive: true, force: true });
|
await rm(target, { recursive: true, force: true });
|
||||||
await Promise.all(Object.entries(entries).map(async ([file, content]) => {
|
await Promise.all(Object.entries(entries).map(async ([file, content]) => {
|
||||||
const dest = join("dist", target, file);
|
const dest = join("dist", target, file);
|
||||||
@ -124,7 +178,6 @@ async function buildPluginZip(target, files, shouldZip) {
|
|||||||
|
|
||||||
console.info("Unpacked Extension written to dist/" + target);
|
console.info("Unpacked Extension written to dist/" + target);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const appendCssRuntime = readFile("dist/Vencord.user.css", "utf-8").then(content => {
|
const appendCssRuntime = readFile("dist/Vencord.user.css", "utf-8").then(content => {
|
||||||
const cssRuntime = `
|
const cssRuntime = `
|
||||||
@ -141,8 +194,9 @@ const appendCssRuntime = readFile("dist/Vencord.user.css", "utf-8").then(content
|
|||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
appendCssRuntime,
|
appendCssRuntime,
|
||||||
buildPluginZip("extension.zip", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"], true),
|
buildExtension("chromium-unpacked", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"]),
|
||||||
buildPluginZip("chromium-unpacked", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"], false),
|
buildExtension("firefox-unpacked", ["content.js", "manifestv2.json", "icon.png"], true),
|
||||||
buildPluginZip("firefox-unpacked", ["background.js", "content.js", "manifestv2.json", "icon.png"], false),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
Zip.sync.zip("dist/chromium-unpacked").compress().save("dist/extension.zip");
|
||||||
|
console.info("Packed Chromium Extension written to dist/extension.zip");
|
||||||
|
@ -27,17 +27,21 @@ import { promisify } from "util";
|
|||||||
|
|
||||||
// wtf is this assert syntax
|
// wtf is this assert syntax
|
||||||
import PackageJSON from "../../package.json" assert { type: "json" };
|
import PackageJSON from "../../package.json" assert { type: "json" };
|
||||||
|
import { getPluginTarget } from "../utils.mjs";
|
||||||
|
|
||||||
export const VERSION = PackageJSON.version;
|
export const VERSION = PackageJSON.version;
|
||||||
export const BUILD_TIMESTAMP = Date.now();
|
// https://reproducible-builds.org/docs/source-date-epoch/
|
||||||
|
export const BUILD_TIMESTAMP = Number(process.env.SOURCE_DATE_EPOCH) || Date.now();
|
||||||
export const watch = process.argv.includes("--watch");
|
export const watch = process.argv.includes("--watch");
|
||||||
export const isStandalone = JSON.stringify(process.argv.includes("--standalone"));
|
export const isStandalone = JSON.stringify(process.argv.includes("--standalone"));
|
||||||
export const gitHash = execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
|
export const updaterDisabled = JSON.stringify(process.argv.includes("--disable-updater"));
|
||||||
|
export const gitHash = process.env.VENCORD_HASH || execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
|
||||||
export const banner = {
|
export const banner = {
|
||||||
js: `
|
js: `
|
||||||
// Vencord ${gitHash}
|
// Vencord ${gitHash}
|
||||||
// Standalone: ${isStandalone}
|
// Standalone: ${isStandalone}
|
||||||
// Platform: ${isStandalone === "false" ? process.platform : "Universal"}
|
// Platform: ${isStandalone === "false" ? process.platform : "Universal"}
|
||||||
|
// Updater disabled: ${updaterDisabled}
|
||||||
`.trim()
|
`.trim()
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -81,14 +85,13 @@ export const globPlugins = kind => ({
|
|||||||
if (file.startsWith("_") || file.startsWith(".")) continue;
|
if (file.startsWith("_") || file.startsWith(".")) continue;
|
||||||
if (file === "index.ts") continue;
|
if (file === "index.ts") continue;
|
||||||
|
|
||||||
const fileBits = file.split(".");
|
const target = getPluginTarget(file);
|
||||||
if (fileBits.length > 2 && ["ts", "tsx"].includes(fileBits.at(-1))) {
|
if (target) {
|
||||||
const mod = fileBits.at(-2);
|
if (target === "dev" && !watch) continue;
|
||||||
if (mod === "dev" && !watch) continue;
|
if (target === "web" && kind === "discordDesktop") continue;
|
||||||
if (mod === "web" && kind === "discordDesktop") continue;
|
if (target === "desktop" && kind === "web") continue;
|
||||||
if (mod === "desktop" && kind === "web") continue;
|
if (target === "discordDesktop" && kind !== "discordDesktop") continue;
|
||||||
if (mod === "discordDesktop" && kind !== "discordDesktop") continue;
|
if (target === "vencordDesktop" && kind !== "vencordDesktop") continue;
|
||||||
if (mod === "vencordDesktop" && kind !== "vencordDesktop") continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mod = `p${i}`;
|
const mod = `p${i}`;
|
||||||
@ -133,11 +136,14 @@ export const gitRemotePlugin = {
|
|||||||
namespace: "git-remote", path: args.path
|
namespace: "git-remote", path: args.path
|
||||||
}));
|
}));
|
||||||
build.onLoad({ filter, namespace: "git-remote" }, async () => {
|
build.onLoad({ filter, namespace: "git-remote" }, async () => {
|
||||||
|
let remote = process.env.VENCORD_REMOTE;
|
||||||
|
if (!remote) {
|
||||||
const res = await promisify(exec)("git remote get-url origin", { encoding: "utf-8" });
|
const res = await promisify(exec)("git remote get-url origin", { encoding: "utf-8" });
|
||||||
const remote = res.stdout.trim()
|
remote = res.stdout.trim()
|
||||||
.replace("https://github.com/", "")
|
.replace("https://github.com/", "")
|
||||||
.replace("git@github.com:", "")
|
.replace("git@github.com:", "")
|
||||||
.replace(/.git$/, "");
|
.replace(/.git$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
return { contents: `export default "${remote}"` };
|
return { contents: `export default "${remote}"` };
|
||||||
});
|
});
|
||||||
|
@ -21,6 +21,8 @@ import { access, readFile } from "fs/promises";
|
|||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import { BigIntLiteral, createSourceFile, Identifier, isArrayLiteralExpression, isCallExpression, isExportAssignment, isIdentifier, isObjectLiteralExpression, isPropertyAccessExpression, isPropertyAssignment, isSatisfiesExpression, isStringLiteral, isVariableStatement, NamedDeclaration, NodeArray, ObjectLiteralExpression, ScriptTarget, StringLiteral, SyntaxKind } from "typescript";
|
import { BigIntLiteral, createSourceFile, Identifier, isArrayLiteralExpression, isCallExpression, isExportAssignment, isIdentifier, isObjectLiteralExpression, isPropertyAccessExpression, isPropertyAssignment, isSatisfiesExpression, isStringLiteral, isVariableStatement, NamedDeclaration, NodeArray, ObjectLiteralExpression, ScriptTarget, StringLiteral, SyntaxKind } from "typescript";
|
||||||
|
|
||||||
|
import { getPluginTarget } from "./utils.mjs";
|
||||||
|
|
||||||
interface Dev {
|
interface Dev {
|
||||||
name: string;
|
name: string;
|
||||||
id: string;
|
id: string;
|
||||||
@ -157,14 +159,17 @@ async function parseFile(fileName: string) {
|
|||||||
|
|
||||||
if (!data.name || !data.description || !data.authors) throw fail("name, description or authors are missing");
|
if (!data.name || !data.description || !data.authors) throw fail("name, description or authors are missing");
|
||||||
|
|
||||||
const fileBits = fileName.split(".");
|
const target = getPluginTarget(fileName);
|
||||||
if (fileBits.length > 2 && ["ts", "tsx"].includes(fileBits.at(-1)!)) {
|
if (target) {
|
||||||
const mod = fileBits.at(-2)!;
|
if (!["web", "discordDesktop", "vencordDesktop", "desktop", "dev"].includes(target)) throw fail(`invalid target ${target}`);
|
||||||
if (!["web", "discordDesktop", "vencordDesktop", "dev"].includes(mod)) throw fail(`invalid target ${fileBits.at(-2)}`);
|
data.target = target as any;
|
||||||
data.target = mod as any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
let readme = "";
|
||||||
|
try {
|
||||||
|
readme = readFileSync(join(fileName, "..", "README.md"), "utf-8");
|
||||||
|
} catch { }
|
||||||
|
return [data, readme] as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw fail("no default export called 'definePlugin' found");
|
throw fail("no default export called 'definePlugin' found");
|
||||||
@ -193,18 +198,24 @@ function isPluginFile({ name }: { name: string; }) {
|
|||||||
(async () => {
|
(async () => {
|
||||||
parseDevs();
|
parseDevs();
|
||||||
|
|
||||||
const plugins = ["src/plugins", "src/plugins/_core"].flatMap(dir =>
|
const plugins = [] as PluginData[];
|
||||||
|
const readmes = {} as Record<string, string>;
|
||||||
|
|
||||||
|
await Promise.all(["src/plugins", "src/plugins/_core"].flatMap(dir =>
|
||||||
readdirSync(dir, { withFileTypes: true })
|
readdirSync(dir, { withFileTypes: true })
|
||||||
.filter(isPluginFile)
|
.filter(isPluginFile)
|
||||||
.map(async dirent =>
|
.map(async dirent => {
|
||||||
parseFile(await getEntryPoint(dir, dirent))
|
const [data, readme] = await parseFile(await getEntryPoint(dir, dirent));
|
||||||
)
|
plugins.push(data);
|
||||||
);
|
if (readme) readmes[data.name] = readme;
|
||||||
|
})
|
||||||
|
));
|
||||||
|
|
||||||
const data = JSON.stringify(await Promise.all(plugins));
|
const data = JSON.stringify(plugins);
|
||||||
|
|
||||||
if (process.argv.length > 2) {
|
if (process.argv.length > 3) {
|
||||||
writeFileSync(process.argv[2], data);
|
writeFileSync(process.argv[2], data);
|
||||||
|
writeFileSync(process.argv[3], JSON.stringify(readmes));
|
||||||
} else {
|
} else {
|
||||||
console.log(data);
|
console.log(data);
|
||||||
}
|
}
|
||||||
|
@ -171,7 +171,7 @@ page.on("console", async e => {
|
|||||||
plugin,
|
plugin,
|
||||||
type,
|
type,
|
||||||
id,
|
id,
|
||||||
match: regex,
|
match: regex.replace(/\[A-Za-z_\$\]\[\\w\$\]\*/g, "\\i"),
|
||||||
error: cause
|
error: cause
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
30
scripts/utils.mjs
Normal file
30
scripts/utils.mjs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} filePath
|
||||||
|
* @returns {string | null}
|
||||||
|
*/
|
||||||
|
export function getPluginTarget(filePath) {
|
||||||
|
const pathParts = filePath.split(/[/\\]/);
|
||||||
|
if (/^index\.tsx?$/.test(pathParts.at(-1))) pathParts.pop();
|
||||||
|
|
||||||
|
const identifier = pathParts.at(-1).replace(/\.tsx?$/, "");
|
||||||
|
const identiferBits = identifier.split(".");
|
||||||
|
return identiferBits.length === 1 ? null : identiferBits.at(-1);
|
||||||
|
}
|
@ -48,7 +48,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
addThemeChangeListener(cb: () => void) {
|
addThemeChangeListener(cb: () => void) {
|
||||||
ipcRenderer.on(IpcEvents.THEME_UPDATE, cb);
|
ipcRenderer.on(IpcEvents.THEME_UPDATE, () => cb());
|
||||||
},
|
},
|
||||||
|
|
||||||
openFile: () => invoke<void>(IpcEvents.OPEN_QUICKCSS),
|
openFile: () => invoke<void>(IpcEvents.OPEN_QUICKCSS),
|
||||||
|
21
src/components/CodeBlock.tsx
Normal file
21
src/components/CodeBlock.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { findByPropsLazy } from "@webpack";
|
||||||
|
import { Parser } from "@webpack/common";
|
||||||
|
|
||||||
|
const CodeContainerClasses = findByPropsLazy("markup", "codeContainer");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders code in a Discord codeblock
|
||||||
|
*/
|
||||||
|
export function CodeBlock(props: { content?: string, lang: string; }) {
|
||||||
|
return (
|
||||||
|
<div className={CodeContainerClasses.markup}>
|
||||||
|
{Parser.defaultRules.codeBlock.react(props, null, {})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
113
src/components/PluginSettings/ContributorModal.tsx
Normal file
113
src/components/PluginSettings/ContributorModal.tsx
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "./contributorModal.css";
|
||||||
|
|
||||||
|
import { useSettings } from "@api/Settings";
|
||||||
|
import { classNameFactory } from "@api/Styles";
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { DevsById } from "@utils/constants";
|
||||||
|
import { fetchUserProfile, getTheme, Theme } from "@utils/discord";
|
||||||
|
import { ModalContent, ModalRoot, openModal } from "@utils/modal";
|
||||||
|
import { Forms, MaskedLink, showToast, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common";
|
||||||
|
import { User } from "discord-types/general";
|
||||||
|
|
||||||
|
import Plugins from "~plugins";
|
||||||
|
|
||||||
|
import { PluginCard } from ".";
|
||||||
|
|
||||||
|
const WebsiteIconDark = "/assets/e1e96d89e192de1997f73730db26e94f.svg";
|
||||||
|
const WebsiteIconLight = "/assets/730f58bcfd5a57a5e22460c445a0c6cf.svg";
|
||||||
|
const GithubIconLight = "/assets/3ff98ad75ac94fa883af5ed62d17c459.svg";
|
||||||
|
const GithubIconDark = "/assets/6a853b4c87fce386cbfef4a2efbacb09.svg";
|
||||||
|
|
||||||
|
const cl = classNameFactory("vc-author-modal-");
|
||||||
|
|
||||||
|
export function openContributorModal(user: User) {
|
||||||
|
openModal(modalProps =>
|
||||||
|
<ModalRoot {...modalProps}>
|
||||||
|
<ErrorBoundary>
|
||||||
|
<ModalContent className={cl("root")}>
|
||||||
|
<ContributorModal user={user} />
|
||||||
|
</ModalContent>
|
||||||
|
</ErrorBoundary>
|
||||||
|
</ModalRoot>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function GithubIcon() {
|
||||||
|
const src = getTheme() === Theme.Light ? GithubIconLight : GithubIconDark;
|
||||||
|
return <img src={src} alt="GitHub" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function WebsiteIcon() {
|
||||||
|
const src = getTheme() === Theme.Light ? WebsiteIconLight : WebsiteIconDark;
|
||||||
|
return <img src={src} alt="Website" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContributorModal({ user }: { user: User; }) {
|
||||||
|
useSettings();
|
||||||
|
|
||||||
|
const profile = useStateFromStores([UserProfileStore], () => UserProfileStore.getUserProfile(user.id));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!profile && !user.bot && user.id)
|
||||||
|
fetchUserProfile(user.id);
|
||||||
|
}, [user.id]);
|
||||||
|
|
||||||
|
const githubName = profile?.connectedAccounts?.find(a => a.type === "github")?.name;
|
||||||
|
const website = profile?.connectedAccounts?.find(a => a.type === "domain")?.name;
|
||||||
|
|
||||||
|
const plugins = useMemo(() => {
|
||||||
|
const allPlugins = Object.values(Plugins);
|
||||||
|
const pluginsByAuthor = DevsById[user.id]
|
||||||
|
? allPlugins.filter(p => p.authors.includes(DevsById[user.id]))
|
||||||
|
: allPlugins.filter(p => p.authors.some(a => a.name === user.username));
|
||||||
|
|
||||||
|
return pluginsByAuthor
|
||||||
|
.filter(p => !p.name.endsWith("API"))
|
||||||
|
.sort((a, b) => Number(a.required ?? false) - Number(b.required ?? false));
|
||||||
|
}, [user.id, user.username]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={cl("header")}>
|
||||||
|
<img
|
||||||
|
className={cl("avatar")}
|
||||||
|
src={user.getAvatarURL(void 0, 512, true)}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<Forms.FormTitle tag="h2" className={cl("name")}>{user.username}</Forms.FormTitle>
|
||||||
|
|
||||||
|
<div className={cl("links")}>
|
||||||
|
{website && (
|
||||||
|
<MaskedLink
|
||||||
|
href={"https://" + website}
|
||||||
|
>
|
||||||
|
<WebsiteIcon />
|
||||||
|
</MaskedLink>
|
||||||
|
)}
|
||||||
|
{githubName && (
|
||||||
|
<MaskedLink href={`https://github.com/${githubName}`}>
|
||||||
|
<GithubIcon />
|
||||||
|
</MaskedLink>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={cl("plugins")}>
|
||||||
|
{plugins.map(p =>
|
||||||
|
<PluginCard
|
||||||
|
key={p.name}
|
||||||
|
plugin={p}
|
||||||
|
disabled={p.required ?? false}
|
||||||
|
onRestartNeeded={() => showToast("Restart to apply changes!")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -18,17 +18,16 @@
|
|||||||
|
|
||||||
import { generateId } from "@api/Commands";
|
import { generateId } from "@api/Commands";
|
||||||
import { useSettings } from "@api/Settings";
|
import { useSettings } from "@api/Settings";
|
||||||
import { disableStyle, enableStyle } from "@api/Styles";
|
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Flex } from "@components/Flex";
|
import { Flex } from "@components/Flex";
|
||||||
import { proxyLazy } from "@utils/lazy";
|
import { proxyLazy } from "@utils/lazy";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { classes } from "@utils/misc";
|
import { classes, isObjectEmpty } from "@utils/misc";
|
||||||
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
|
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
|
||||||
import { LazyComponent } from "@utils/react";
|
import { LazyComponent } from "@utils/react";
|
||||||
import { OptionType, Plugin } from "@utils/types";
|
import { OptionType, Plugin } from "@utils/types";
|
||||||
import { findByCode, findByPropsLazy } from "@webpack";
|
import { findByCode, findByPropsLazy } from "@webpack";
|
||||||
import { Button, FluxDispatcher, Forms, React, Text, Tooltip, UserStore, UserUtils } from "@webpack/common";
|
import { Button, Clickable, FluxDispatcher, Forms, React, Text, Tooltip, UserStore, UserUtils } from "@webpack/common";
|
||||||
import { User } from "discord-types/general";
|
import { User } from "discord-types/general";
|
||||||
import { Constructor } from "type-fest";
|
import { Constructor } from "type-fest";
|
||||||
|
|
||||||
@ -41,7 +40,7 @@ import {
|
|||||||
SettingSliderComponent,
|
SettingSliderComponent,
|
||||||
SettingTextComponent
|
SettingTextComponent
|
||||||
} from "./components";
|
} from "./components";
|
||||||
import hideBotTagStyle from "./userPopoutHideBotTag.css?managed";
|
import { openContributorModal } from "./ContributorModal";
|
||||||
|
|
||||||
const UserSummaryItem = LazyComponent(() => findByCode("defaultRenderUser", "showDefaultAvatarsForNullUsers"));
|
const UserSummaryItem = LazyComponent(() => findByCode("defaultRenderUser", "showDefaultAvatarsForNullUsers"));
|
||||||
const AvatarStyles = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar");
|
const AvatarStyles = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar");
|
||||||
@ -89,30 +88,19 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||||||
|
|
||||||
const canSubmit = () => Object.values(errors).every(e => !e);
|
const canSubmit = () => Object.values(errors).every(e => !e);
|
||||||
|
|
||||||
const hasSettings = Boolean(pluginSettings && plugin.options);
|
const hasSettings = Boolean(pluginSettings && plugin.options && !isObjectEmpty(plugin.options));
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
enableStyle(hideBotTagStyle);
|
|
||||||
|
|
||||||
let originalUser: User;
|
|
||||||
(async () => {
|
(async () => {
|
||||||
for (const user of plugin.authors.slice(0, 6)) {
|
for (const user of plugin.authors.slice(0, 6)) {
|
||||||
const author = user.id
|
const author = user.id
|
||||||
? await UserUtils.fetchUser(`${user.id}`)
|
? await UserUtils.fetchUser(`${user.id}`)
|
||||||
// only show name & pfp and no actions so users cannot harass plugin devs for support (send dms, add as friend, etc)
|
|
||||||
.then(u => (originalUser = u, makeDummyUser(u)))
|
|
||||||
.catch(() => makeDummyUser({ username: user.name }))
|
.catch(() => makeDummyUser({ username: user.name }))
|
||||||
: makeDummyUser({ username: user.name });
|
: makeDummyUser({ username: user.name });
|
||||||
|
|
||||||
setAuthors(a => [...a, author]);
|
setAuthors(a => [...a, author]);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return () => {
|
|
||||||
disableStyle(hideBotTagStyle);
|
|
||||||
if (originalUser)
|
|
||||||
FluxDispatcher.dispatch({ type: "USER_UPDATE", user: originalUser });
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
async function saveAndClose() {
|
async function saveAndClose() {
|
||||||
@ -214,6 +202,19 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||||||
showDefaultAvatarsForNullUsers
|
showDefaultAvatarsForNullUsers
|
||||||
showUserPopout
|
showUserPopout
|
||||||
renderMoreUsers={renderMoreUsers}
|
renderMoreUsers={renderMoreUsers}
|
||||||
|
renderUser={(user: User) => (
|
||||||
|
<Clickable
|
||||||
|
className={AvatarStyles.clickableAvatar}
|
||||||
|
onClick={() => openContributorModal(user)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className={AvatarStyles.avatar}
|
||||||
|
src={user.getAvatarURL(void 0, 80, true)}
|
||||||
|
alt={user.username}
|
||||||
|
title={user.username}
|
||||||
|
/>
|
||||||
|
</Clickable>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Forms.FormSection>
|
</Forms.FormSection>
|
||||||
|
@ -33,12 +33,11 @@ export function SettingTextComponent({ option, pluginSettings, definedSettings,
|
|||||||
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
||||||
if (typeof isValid === "string") setError(isValid);
|
if (typeof isValid === "string") setError(isValid);
|
||||||
else if (!isValid) setError("Invalid input provided.");
|
else if (!isValid) setError("Invalid input provided.");
|
||||||
else {
|
else setError(null);
|
||||||
setError(null);
|
|
||||||
setState(newValue);
|
setState(newValue);
|
||||||
onChange(newValue);
|
onChange(newValue);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Forms.FormSection>
|
<Forms.FormSection>
|
||||||
|
57
src/components/PluginSettings/contributorModal.css
Normal file
57
src/components/PluginSettings/contributorModal.css
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
.vc-author-modal-root {
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-author-modal-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-author-modal-name {
|
||||||
|
text-transform: none;
|
||||||
|
flex-grow: 0;
|
||||||
|
background: var(--background-tertiary);
|
||||||
|
border-radius: 0 9999px 9999px 0;
|
||||||
|
padding: 6px 0.8em 6px 0.5em;
|
||||||
|
font-size: 20px;
|
||||||
|
height: 20px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-author-modal-name::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
width: 16px;
|
||||||
|
background: var(--background-tertiary);
|
||||||
|
z-index: -1;
|
||||||
|
left: -16px;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-author-modal-avatar {
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-author-modal-links {
|
||||||
|
margin-left: auto;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-author-modal-links img {
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 4px solid var(--background-tertiary);
|
||||||
|
box-sizing: border-box
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-author-modal-plugins {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.5em;
|
||||||
|
}
|
@ -28,7 +28,7 @@ import { SettingsTab } from "@components/VencordSettings/shared";
|
|||||||
import { ChangeList } from "@utils/ChangeList";
|
import { ChangeList } from "@utils/ChangeList";
|
||||||
import { Logger } from "@utils/Logger";
|
import { Logger } from "@utils/Logger";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { classes } from "@utils/misc";
|
import { classes, isObjectEmpty } from "@utils/misc";
|
||||||
import { openModalLazy } from "@utils/modal";
|
import { openModalLazy } from "@utils/modal";
|
||||||
import { LazyComponent, useAwaiter } from "@utils/react";
|
import { LazyComponent, useAwaiter } from "@utils/react";
|
||||||
import { Plugin } from "@utils/types";
|
import { Plugin } from "@utils/types";
|
||||||
@ -91,7 +91,7 @@ interface PluginCardProps extends React.HTMLProps<HTMLDivElement> {
|
|||||||
isNew?: boolean;
|
isNew?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave, isNew }: PluginCardProps) {
|
export function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave, isNew }: PluginCardProps) {
|
||||||
const settings = Settings.plugins[plugin.name];
|
const settings = Settings.plugins[plugin.name];
|
||||||
|
|
||||||
const isEnabled = () => settings.enabled ?? false;
|
const isEnabled = () => settings.enabled ?? false;
|
||||||
@ -161,7 +161,7 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe
|
|||||||
onMouseLeave={onMouseLeave}
|
onMouseLeave={onMouseLeave}
|
||||||
infoButton={
|
infoButton={
|
||||||
<button role="switch" onClick={() => openModal()} className={classes(ButtonClasses.button, cl("info-button"))}>
|
<button role="switch" onClick={() => openModal()} className={classes(ButtonClasses.button, cl("info-button"))}>
|
||||||
{plugin.options
|
{plugin.options && !isObjectEmpty(plugin.options)
|
||||||
? <CogWheel />
|
? <CogWheel />
|
||||||
: <InfoIcon width="24" height="24" />}
|
: <InfoIcon width="24" height="24" />}
|
||||||
</button>
|
</button>
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
[class|="userPopoutOuter"] [class*="botTag"] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
@ -17,6 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { CheckedTextInput } from "@components/CheckedTextInput";
|
import { CheckedTextInput } from "@components/CheckedTextInput";
|
||||||
|
import { CodeBlock } from "@components/CodeBlock";
|
||||||
import { debounce } from "@utils/debounce";
|
import { debounce } from "@utils/debounce";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
|
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
|
||||||
@ -299,7 +300,7 @@ function PatchHelper() {
|
|||||||
{!!(find && match && replacement) && (
|
{!!(find && match && replacement) && (
|
||||||
<>
|
<>
|
||||||
<Forms.FormTitle className={Margins.top20}>Code</Forms.FormTitle>
|
<Forms.FormTitle className={Margins.top20}>Code</Forms.FormTitle>
|
||||||
<div style={{ userSelect: "text" }}>{Parser.parse(makeCodeblock(code, "ts"))}</div>
|
<CodeBlock lang="js" content={code} />
|
||||||
<Button onClick={() => Clipboard.copy(code)}>Copy to Clipboard</Button>
|
<Button onClick={() => Clipboard.copy(code)}>Copy to Clipboard</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -18,8 +18,10 @@
|
|||||||
|
|
||||||
import { useSettings } from "@api/Settings";
|
import { useSettings } from "@api/Settings";
|
||||||
import { classNameFactory } from "@api/Styles";
|
import { classNameFactory } from "@api/Styles";
|
||||||
|
import { ErrorCard } from "@components/ErrorCard";
|
||||||
import { Flex } from "@components/Flex";
|
import { Flex } from "@components/Flex";
|
||||||
import { Link } from "@components/Link";
|
import { Link } from "@components/Link";
|
||||||
|
import { IsFirefox } from "@utils/constants";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { classes } from "@utils/misc";
|
import { classes } from "@utils/misc";
|
||||||
import { showItemInFolder } from "@utils/native";
|
import { showItemInFolder } from "@utils/native";
|
||||||
@ -231,7 +233,7 @@ function ThemesTab() {
|
|||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
onChange={onFileUpload}
|
onChange={onFileUpload}
|
||||||
multiple={true}
|
multiple={true}
|
||||||
filters={[{ extensions: ["*.css"] }]}
|
filters={[{ extensions: ["css"] }]}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
@ -249,12 +251,14 @@ function ThemesTab() {
|
|||||||
>
|
>
|
||||||
Load missing Themes
|
Load missing Themes
|
||||||
</Button>
|
</Button>
|
||||||
|
{!IsFirefox && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => VencordNative.quickCss.openEditor()}
|
onClick={() => VencordNative.quickCss.openEditor()}
|
||||||
size={Button.Sizes.SMALL}
|
size={Button.Sizes.SMALL}
|
||||||
>
|
>
|
||||||
Edit QuickCSS
|
Edit QuickCSS
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@ -316,6 +320,15 @@ function ThemesTab() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsTab title="Themes">
|
<SettingsTab title="Themes">
|
||||||
|
{IsFirefox && (
|
||||||
|
<ErrorCard>
|
||||||
|
<Forms.FormTitle tag="h5">Warning</Forms.FormTitle>
|
||||||
|
<Forms.FormText>
|
||||||
|
You are using Firefox. Expect the vast majority of themes to not work.
|
||||||
|
If this is a problem, use a chromium browser or Discord Desktop / Vesktop.
|
||||||
|
</Forms.FormText>
|
||||||
|
</ErrorCard>
|
||||||
|
)}
|
||||||
<TabBar
|
<TabBar
|
||||||
type="top"
|
type="top"
|
||||||
look="brand"
|
look="brand"
|
||||||
|
@ -249,4 +249,4 @@ function Updater() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default IS_WEB ? null : wrapTab(Updater, "Updater");
|
export default IS_UPDATER_DISABLED ? null : wrapTab(Updater, "Updater");
|
||||||
|
@ -21,6 +21,7 @@ import { Settings, useSettings } from "@api/Settings";
|
|||||||
import { classNameFactory } from "@api/Styles";
|
import { classNameFactory } from "@api/Styles";
|
||||||
import DonateButton from "@components/DonateButton";
|
import DonateButton from "@components/DonateButton";
|
||||||
import { ErrorCard } from "@components/ErrorCard";
|
import { ErrorCard } from "@components/ErrorCard";
|
||||||
|
import { IsFirefox } from "@utils/constants";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { identity } from "@utils/misc";
|
import { identity } from "@utils/misc";
|
||||||
import { relaunch, showItemInFolder } from "@utils/native";
|
import { relaunch, showItemInFolder } from "@utils/native";
|
||||||
@ -109,12 +110,14 @@ function VencordSettings() {
|
|||||||
Restart Client
|
Restart Client
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{!IsFirefox && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => VencordNative.quickCss.openEditor()}
|
onClick={() => VencordNative.quickCss.openEditor()}
|
||||||
size={Button.Sizes.SMALL}
|
size={Button.Sizes.SMALL}
|
||||||
disabled={settingsDir === "Loading..."}>
|
disabled={settingsDir === "Loading..."}>
|
||||||
Open QuickCSS File
|
Open QuickCSS File
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
{!IS_WEB && (
|
{!IS_WEB && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => showItemInFolder(settingsDir)}
|
onClick={() => showItemInFolder(settingsDir)}
|
||||||
@ -257,7 +260,11 @@ function DonateCard({ image }: DonateCardProps) {
|
|||||||
src={image}
|
src={image}
|
||||||
alt=""
|
alt=""
|
||||||
height={128}
|
height={128}
|
||||||
style={{ marginLeft: "auto", transform: image === DEFAULT_DONATE_IMAGE ? "rotate(10deg)" : "" }}
|
style={{
|
||||||
|
imageRendering: image === SHIGGY_DONATE_IMAGE ? "pixelated" : void 0,
|
||||||
|
marginLeft: "auto",
|
||||||
|
transform: image === DEFAULT_DONATE_IMAGE ? "rotate(10deg)" : void 0
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
transition: 0.1s ease-out;
|
transition: 0.1s ease-out;
|
||||||
transition-property: box-shadow, transform, background, opacity;
|
transition-property: box-shadow, transform, background, opacity;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-addon-card-disabled {
|
.vc-addon-card-disabled {
|
||||||
|
8
src/globals.d.ts
vendored
8
src/globals.d.ts
vendored
@ -33,10 +33,12 @@ declare global {
|
|||||||
* replace: `${IS_WEB}?foo:bar`
|
* replace: `${IS_WEB}?foo:bar`
|
||||||
*/
|
*/
|
||||||
export var IS_WEB: boolean;
|
export var IS_WEB: boolean;
|
||||||
|
export var IS_EXTENSION: boolean;
|
||||||
export var IS_DEV: boolean;
|
export var IS_DEV: boolean;
|
||||||
export var IS_STANDALONE: boolean;
|
export var IS_STANDALONE: boolean;
|
||||||
|
export var IS_UPDATER_DISABLED: boolean;
|
||||||
export var IS_DISCORD_DESKTOP: boolean;
|
export var IS_DISCORD_DESKTOP: boolean;
|
||||||
export var IS_VENCORD_DESKTOP: boolean;
|
export var IS_VESKTOP: boolean;
|
||||||
export var VERSION: string;
|
export var VERSION: string;
|
||||||
export var BUILD_TIMESTAMP: number;
|
export var BUILD_TIMESTAMP: number;
|
||||||
|
|
||||||
@ -58,8 +60,8 @@ declare global {
|
|||||||
* If you really must use it, mark your plugin as Desktop App only by naming it Foo.desktop.ts(x)
|
* If you really must use it, mark your plugin as Desktop App only by naming it Foo.desktop.ts(x)
|
||||||
*/
|
*/
|
||||||
export var DiscordNative: any;
|
export var DiscordNative: any;
|
||||||
export var VencordDesktop: any;
|
export var Vesktop: any;
|
||||||
export var VencordDesktopNative: any;
|
export var VesktopNative: any;
|
||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
webpackChunkdiscord_app: {
|
webpackChunkdiscord_app: {
|
||||||
|
@ -23,7 +23,7 @@ import { ensureSafePath, getSettings } from "./ipcMain";
|
|||||||
import { IS_VANILLA, THEMES_DIR } from "./utils/constants";
|
import { IS_VANILLA, THEMES_DIR } from "./utils/constants";
|
||||||
import { installExt } from "./utils/extensions";
|
import { installExt } from "./utils/extensions";
|
||||||
|
|
||||||
if (IS_VENCORD_DESKTOP || !IS_VANILLA) {
|
if (IS_VESKTOP || !IS_VANILLA) {
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
// Source Maps! Maybe there's a better way but since the renderer is executed
|
// 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
|
// from a string I don't think any other form of sourcemaps would work
|
||||||
|
@ -27,7 +27,7 @@ import { mkdirSync, readFileSync, watch } from "fs";
|
|||||||
import { open, readdir, readFile, writeFile } from "fs/promises";
|
import { open, readdir, readFile, writeFile } from "fs/promises";
|
||||||
import { join, normalize } from "path";
|
import { join, normalize } from "path";
|
||||||
|
|
||||||
import monacoHtml from "~fileContent/../components/monacoWin.html;base64";
|
import monacoHtml from "~fileContent/monacoWin.html;base64";
|
||||||
|
|
||||||
import { getThemeInfo, stripBOM, UserThemeHeader } from "./themes";
|
import { getThemeInfo, stripBOM, UserThemeHeader } from "./themes";
|
||||||
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, SETTINGS_FILE, THEMES_DIR } from "./utils/constants";
|
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, SETTINGS_FILE, THEMES_DIR } from "./utils/constants";
|
||||||
@ -53,10 +53,12 @@ async function listThemes(): Promise<UserThemeHeader[]> {
|
|||||||
const themeInfo: UserThemeHeader[] = [];
|
const themeInfo: UserThemeHeader[] = [];
|
||||||
|
|
||||||
for (const fileName of files) {
|
for (const fileName of files) {
|
||||||
|
if (!fileName.endsWith(".css")) continue;
|
||||||
|
|
||||||
const data = await getThemeData(fileName).then(stripBOM).catch(() => null);
|
const data = await getThemeData(fileName).then(stripBOM).catch(() => null);
|
||||||
if (!data) continue;
|
if (data == null) continue;
|
||||||
const parsed = getThemeInfo(data, fileName);
|
|
||||||
themeInfo.push(parsed);
|
themeInfo.push(getThemeInfo(data, fileName));
|
||||||
}
|
}
|
||||||
|
|
||||||
return themeInfo;
|
return themeInfo;
|
||||||
|
@ -22,6 +22,28 @@ import { readFile } from "fs/promises";
|
|||||||
import { request } from "https";
|
import { request } from "https";
|
||||||
import { basename, normalize } from "path";
|
import { basename, normalize } from "path";
|
||||||
|
|
||||||
|
import { getSettings } from "./ipcMain";
|
||||||
|
|
||||||
|
// FixSpotifyEmbeds
|
||||||
|
app.on("browser-window-created", (_, win) => {
|
||||||
|
win.webContents.on("frame-created", (_, { frame }) => {
|
||||||
|
frame.once("dom-ready", () => {
|
||||||
|
if (frame.url.startsWith("https://open.spotify.com/embed/")) {
|
||||||
|
const settings = getSettings().plugins?.FixSpotifyEmbeds;
|
||||||
|
if (!settings?.enabled) return;
|
||||||
|
|
||||||
|
frame.executeJavaScript(`
|
||||||
|
const original = Audio.prototype.play;
|
||||||
|
Audio.prototype.play = function() {
|
||||||
|
this.volume = ${(settings.volume / 100) || 0.1};
|
||||||
|
return original.apply(this, arguments);
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// #region OpenInApp
|
// #region OpenInApp
|
||||||
// These links don't support CORS, so this has to be native
|
// These links don't support CORS, so this has to be native
|
||||||
const validRedirectUrls = /^https:\/\/(spotify\.link|s\.team)\/.+$/;
|
const validRedirectUrls = /^https:\/\/(spotify\.link|s\.team)\/.+$/;
|
||||||
|
@ -16,22 +16,10 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { app, autoUpdater } from "electron";
|
import { app } from "electron";
|
||||||
import { existsSync, mkdirSync, readdirSync, renameSync, statSync, writeFileSync } from "fs";
|
import { existsSync, mkdirSync, readdirSync, renameSync, statSync, writeFileSync } from "fs";
|
||||||
import { basename, dirname, join } from "path";
|
import { basename, dirname, join } from "path";
|
||||||
|
|
||||||
const { setAppUserModelId } = app;
|
|
||||||
|
|
||||||
// Apparently requiring Discords updater too early leads into issues,
|
|
||||||
// copied this workaround from powerCord
|
|
||||||
app.setAppUserModelId = function (id: string) {
|
|
||||||
app.setAppUserModelId = setAppUserModelId;
|
|
||||||
|
|
||||||
setAppUserModelId.call(this, id);
|
|
||||||
|
|
||||||
patchUpdater();
|
|
||||||
};
|
|
||||||
|
|
||||||
function isNewer($new: string, old: string) {
|
function isNewer($new: string, old: string) {
|
||||||
const newParts = $new.slice(4).split(".").map(Number);
|
const newParts = $new.slice(4).split(".").map(Number);
|
||||||
const oldParts = old.slice(4).split(".").map(Number);
|
const oldParts = old.slice(4).split(".").map(Number);
|
||||||
@ -77,23 +65,6 @@ function patchLatest() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Windows Host Updates install to a new folder app-{HOST_VERSION}, so we
|
// Try to patch latest on before-quit
|
||||||
// need to reinject
|
// Discord's Win32 updater will call app.quit() on restart and open new version on will-quit
|
||||||
function patchUpdater() {
|
app.on("before-quit", patchLatest);
|
||||||
try {
|
|
||||||
const autoStartScript = join(require.main!.filename, "..", "autoStart", "win32.js");
|
|
||||||
const { update } = require(autoStartScript);
|
|
||||||
|
|
||||||
require.cache[autoStartScript]!.exports.update = function () {
|
|
||||||
update.apply(this, arguments);
|
|
||||||
patchLatest();
|
|
||||||
};
|
|
||||||
} catch {
|
|
||||||
// OpenAsar uses electrons autoUpdater on Windows
|
|
||||||
const { quitAndInstall } = autoUpdater;
|
|
||||||
autoUpdater.quitAndInstall = function () {
|
|
||||||
patchLatest();
|
|
||||||
quitAndInstall.call(this);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -16,4 +16,5 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
if (!IS_UPDATER_DISABLED)
|
||||||
import(IS_STANDALONE ? "./http" : "./git");
|
import(IS_STANDALONE ? "./http" : "./git");
|
||||||
|
@ -27,8 +27,8 @@ export default definePlugin({
|
|||||||
{
|
{
|
||||||
find: "Messages.DISCODO_DISABLED",
|
find: "Messages.DISCODO_DISABLED",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(Messages\.DISCODO_DISABLED\);return)(.*?homeIcon.*?)(\}function)/,
|
match: /(Messages\.DISCODO_DISABLED.+?return)(\(.{0,75}?tutorialContainer.+?}\))(?=}function)/,
|
||||||
replace: "$1[$2].concat(Vencord.Api.ServerList.renderAll(Vencord.Api.ServerList.ServerListRenderPosition.Above))$3"
|
replace: "$1[$2].concat(Vencord.Api.ServerList.renderAll(Vencord.Api.ServerList.ServerListRenderPosition.Above))"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -103,7 +103,7 @@ export default definePlugin({
|
|||||||
element: require("@components/VencordSettings/ThemesTab").default,
|
element: require("@components/VencordSettings/ThemesTab").default,
|
||||||
className: "vc-themes"
|
className: "vc-themes"
|
||||||
},
|
},
|
||||||
!IS_WEB && {
|
!IS_UPDATER_DISABLED && {
|
||||||
section: "VencordUpdater",
|
section: "VencordUpdater",
|
||||||
label: "Updater",
|
label: "Updater",
|
||||||
element: require("@components/VencordSettings/UpdaterTab").default,
|
element: require("@components/VencordSettings/UpdaterTab").default,
|
||||||
@ -127,13 +127,6 @@ export default definePlugin({
|
|||||||
element: require("@components/VencordSettings/PatchHelperTab").default,
|
element: require("@components/VencordSettings/PatchHelperTab").default,
|
||||||
className: "vc-patch-helper"
|
className: "vc-patch-helper"
|
||||||
},
|
},
|
||||||
// TODO: make this use customSections
|
|
||||||
IS_VENCORD_DESKTOP && {
|
|
||||||
section: "VencordDesktop",
|
|
||||||
label: "Desktop Settings",
|
|
||||||
element: VencordDesktop.Components.Settings,
|
|
||||||
className: "vc-desktop-settings"
|
|
||||||
},
|
|
||||||
...this.customSections.map(func => func(ID)),
|
...this.customSections.map(func => func(ID)),
|
||||||
{
|
{
|
||||||
section: ID.DIVIDER
|
section: ID.DIVIDER
|
||||||
@ -175,7 +168,7 @@ export default definePlugin({
|
|||||||
get additionalInfo() {
|
get additionalInfo() {
|
||||||
if (IS_DEV) return " (Dev)";
|
if (IS_DEV) return " (Dev)";
|
||||||
if (IS_WEB) return " (Web)";
|
if (IS_WEB) return " (Web)";
|
||||||
if (IS_VENCORD_DESKTOP) return ` (VencordDesktop v${VencordDesktopNative.app.getVersion()})`;
|
if (IS_VESKTOP) return ` (Vesktop v${VesktopNative.app.getVersion()})`;
|
||||||
if (IS_STANDALONE) return " (Standalone)";
|
if (IS_STANDALONE) return " (Standalone)";
|
||||||
return "";
|
return "";
|
||||||
},
|
},
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { DataStore } from "@api/index";
|
import { DataStore } from "@api/index";
|
||||||
import { Devs, SUPPORT_CHANNEL_ID } from "@utils/constants";
|
import { Devs, IsFirefox, SUPPORT_CHANNEL_ID } from "@utils/constants";
|
||||||
import { isPluginDev } from "@utils/misc";
|
import { isPluginDev } from "@utils/misc";
|
||||||
import { makeCodeblock } from "@utils/text";
|
import { makeCodeblock } from "@utils/text";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
@ -27,9 +27,10 @@ import { Alerts, Forms, UserStore } from "@webpack/common";
|
|||||||
import gitHash from "~git-hash";
|
import gitHash from "~git-hash";
|
||||||
import plugins from "~plugins";
|
import plugins from "~plugins";
|
||||||
|
|
||||||
import settings from "./_core/settings";
|
import settings from "./settings";
|
||||||
|
|
||||||
const REMEMBER_DISMISS_KEY = "Vencord-SupportHelper-Dismiss";
|
const REMEMBER_DISMISS_KEY = "Vencord-SupportHelper-Dismiss";
|
||||||
|
const FIREFOX_DISMISS_KEY = "Vencord-Firefox-Warning-Dismiss";
|
||||||
|
|
||||||
const AllowedChannelIds = [
|
const AllowedChannelIds = [
|
||||||
SUPPORT_CHANNEL_ID,
|
SUPPORT_CHANNEL_ID,
|
||||||
@ -48,12 +49,12 @@ export default definePlugin({
|
|||||||
name: "vencord-debug",
|
name: "vencord-debug",
|
||||||
description: "Send Vencord Debug info",
|
description: "Send Vencord Debug info",
|
||||||
predicate: ctx => AllowedChannelIds.includes(ctx.channel.id),
|
predicate: ctx => AllowedChannelIds.includes(ctx.channel.id),
|
||||||
execute() {
|
async execute() {
|
||||||
const { RELEASE_CHANNEL } = window.GLOBAL_ENV;
|
const { RELEASE_CHANNEL } = window.GLOBAL_ENV;
|
||||||
|
|
||||||
const client = (() => {
|
const client = (() => {
|
||||||
if (IS_DISCORD_DESKTOP) return `Discord Desktop v${DiscordNative.app.getVersion()}`;
|
if (IS_DISCORD_DESKTOP) return `Discord Desktop v${DiscordNative.app.getVersion()}`;
|
||||||
if (IS_VENCORD_DESKTOP) return `Vencord Desktop v${VencordDesktopNative.app.getVersion()}`;
|
if (IS_VESKTOP) return `Vesktop v${VesktopNative.app.getVersion()}`;
|
||||||
if ("armcord" in window) return `ArmCord v${window.armcord.version}`;
|
if ("armcord" in window) return `ArmCord v${window.armcord.version}`;
|
||||||
|
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
@ -75,6 +76,10 @@ export default definePlugin({
|
|||||||
OpenAsar: "openasar" in window,
|
OpenAsar: "openasar" in window,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (IS_DISCORD_DESKTOP) {
|
||||||
|
info["Last Crash Reason"] = (await DiscordNative.processUtils.getLastCrash())?.rendererCrashReason ?? "N/A";
|
||||||
|
}
|
||||||
|
|
||||||
const debugInfo = `
|
const debugInfo = `
|
||||||
**Vencord Debug Info**
|
**Vencord Debug Info**
|
||||||
>>> ${Object.entries(info).map(([k, v]) => `${k}: ${v}`).join("\n")}
|
>>> ${Object.entries(info).map(([k, v]) => `${k}: ${v}`).join("\n")}
|
||||||
@ -111,6 +116,22 @@ ${makeCodeblock(enabledPlugins.join(", ") + "\n\n" + enabledApiPlugins.join(", "
|
|||||||
onConfirm: rememberDismiss
|
onConfirm: rememberDismiss
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (IsFirefox) {
|
||||||
|
const rememberDismiss = () => DataStore.set(FIREFOX_DISMISS_KEY, true);
|
||||||
|
|
||||||
|
Alerts.show({
|
||||||
|
title: "Hold on!",
|
||||||
|
body: <div>
|
||||||
|
<Forms.FormText>You are using Firefox.</Forms.FormText>
|
||||||
|
<Forms.FormText>Due to Firefox's stupid extension guidelines, most themes and many plugins will not function correctly.</Forms.FormText>
|
||||||
|
<Forms.FormText>Do not report bugs. Do not ask for help with broken plugins.</Forms.FormText>
|
||||||
|
<Forms.FormText>Instead, use a chromium browser, Discord Desktop, or Vesktop.</Forms.FormText>
|
||||||
|
</div>,
|
||||||
|
onCancel: rememberDismiss,
|
||||||
|
onConfirm: rememberDismiss
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
@ -34,7 +34,7 @@ export default definePlugin({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: 'preload:"none","aria',
|
find: ".embedGallerySide",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<==(.{1,3})\.alt.{0,20})\?.{0,5}\.Messages\.GIF/,
|
match: /(?<==(.{1,3})\.alt.{0,20})\?.{0,5}\.Messages\.GIF/,
|
||||||
replace:
|
replace:
|
||||||
|
@ -23,7 +23,7 @@ import { Clipboard, Toasts } from "@webpack/common";
|
|||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "BetterRoleDot",
|
name: "BetterRoleDot",
|
||||||
authors: [Devs.Ven],
|
authors: [Devs.Ven, Devs.AutumnVN],
|
||||||
description:
|
description:
|
||||||
"Copy role colour on RoleDot (accessibility setting) click. Also allows using both RoleDot and coloured names simultaneously",
|
"Copy role colour on RoleDot (accessibility setting) click. Also allows using both RoleDot and coloured names simultaneously",
|
||||||
|
|
||||||
@ -43,6 +43,23 @@ export default definePlugin({
|
|||||||
match: /"(?:username|dot)"===\i(?!\.\i)/g,
|
match: /"(?:username|dot)"===\i(?!\.\i)/g,
|
||||||
replace: "true",
|
replace: "true",
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
find: ".ADD_ROLE_A11Y_LABEL",
|
||||||
|
predicate: () => Settings.plugins.BetterRoleDot.copyRoleColorInProfilePopout,
|
||||||
|
replacement: {
|
||||||
|
match: /"dot"===\i/,
|
||||||
|
replace: "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: ".roleVerifiedIcon",
|
||||||
|
predicate: () => Settings.plugins.BetterRoleDot.copyRoleColorInProfilePopout,
|
||||||
|
replacement: {
|
||||||
|
match: /"dot"===\i/,
|
||||||
|
replace: "true"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
@ -50,7 +67,14 @@ export default definePlugin({
|
|||||||
bothStyles: {
|
bothStyles: {
|
||||||
type: OptionType.BOOLEAN,
|
type: OptionType.BOOLEAN,
|
||||||
description: "Show both role dot and coloured names",
|
description: "Show both role dot and coloured names",
|
||||||
|
restartNeeded: true,
|
||||||
default: false,
|
default: false,
|
||||||
|
},
|
||||||
|
copyRoleColorInProfilePopout: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Allow click on role dot in profile popout to copy role color",
|
||||||
|
restartNeeded: true,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ export default definePlugin({
|
|||||||
replace: "$1,vcProps=$2$3+(vcProps.channel.nsfw?' vc-nsfw-img':'')"
|
replace: "$1,vcProps=$2$3+(vcProps.channel.nsfw?' vc-nsfw-img':'')"
|
||||||
}, {
|
}, {
|
||||||
match: /(\.renderAttachments=.+?(.)=this\.props)(.+?\.embedWrapper)/g,
|
match: /(\.renderAttachments=.+?(.)=this\.props)(.+?\.embedWrapper)/g,
|
||||||
replace: "$1,vcProps=$2$3+(vcProps.channel.nsfw?' vc-nsfw-img':'')"
|
replace: "$1,vcProps=$2$3+(vcProps.nsfw?' vc-nsfw-img':'')"
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -127,6 +127,9 @@ export const defaultRules = [
|
|||||||
"redircnt@yandex.*",
|
"redircnt@yandex.*",
|
||||||
"feature@youtube.com",
|
"feature@youtube.com",
|
||||||
"kw@youtube.com",
|
"kw@youtube.com",
|
||||||
|
"si@youtube.com",
|
||||||
|
"pp@youtube.com",
|
||||||
|
"si@youtu.be",
|
||||||
"wt_zmc",
|
"wt_zmc",
|
||||||
"utm_source",
|
"utm_source",
|
||||||
"utm_content",
|
"utm_content",
|
||||||
@ -135,4 +138,5 @@ export const defaultRules = [
|
|||||||
"utm_term",
|
"utm_term",
|
||||||
"si@open.spotify.com",
|
"si@open.spotify.com",
|
||||||
"igshid",
|
"igshid",
|
||||||
|
"share_id@reddit.com",
|
||||||
];
|
];
|
||||||
|
55
src/plugins/copyUserURLs.tsx
Normal file
55
src/plugins/copyUserURLs.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
|
||||||
|
import { LinkIcon } from "@components/Icons";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import definePlugin from "@utils/types";
|
||||||
|
import { Clipboard, Menu } from "@webpack/common";
|
||||||
|
import type { Channel, User } from "discord-types/general";
|
||||||
|
|
||||||
|
interface UserContextProps {
|
||||||
|
channel: Channel;
|
||||||
|
guildId?: string;
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserContextMenuPatch: NavContextMenuPatchCallback = (children, { user }: UserContextProps) => () => {
|
||||||
|
children.push(
|
||||||
|
<Menu.MenuItem
|
||||||
|
id="vc-copy-user-url"
|
||||||
|
label="Copy User URL"
|
||||||
|
action={() => Clipboard.copy(`<https://discord.com/users/${user.id}>`)}
|
||||||
|
icon={LinkIcon}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "CopyUserURLs",
|
||||||
|
authors: [Devs.castdrian],
|
||||||
|
description: "Adds a 'Copy User URL' option to the user context menu.",
|
||||||
|
|
||||||
|
start() {
|
||||||
|
addContextMenuPatch("user-context", UserContextMenuPatch);
|
||||||
|
},
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
removeContextMenuPatch("user-context", UserContextMenuPatch);
|
||||||
|
},
|
||||||
|
});
|
@ -23,10 +23,12 @@ import { Logger } from "@utils/Logger";
|
|||||||
import { closeAllModals } from "@utils/modal";
|
import { closeAllModals } from "@utils/modal";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { maybePromptToUpdate } from "@utils/updater";
|
import { maybePromptToUpdate } from "@utils/updater";
|
||||||
|
import { findByPropsLazy } from "@webpack";
|
||||||
import { FluxDispatcher, NavigationRouter } from "@webpack/common";
|
import { FluxDispatcher, NavigationRouter } from "@webpack/common";
|
||||||
import type { ReactElement } from "react";
|
import type { ReactElement } from "react";
|
||||||
|
|
||||||
const CrashHandlerLogger = new Logger("CrashHandler");
|
const CrashHandlerLogger = new Logger("CrashHandler");
|
||||||
|
const ModalStack = findByPropsLazy("pushLazy", "popAll");
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
const settings = definePluginSettings({
|
||||||
attemptToPreventCrashes: {
|
attemptToPreventCrashes: {
|
||||||
@ -51,8 +53,6 @@ export default definePlugin({
|
|||||||
authors: [Devs.Nuckyz],
|
authors: [Devs.Nuckyz],
|
||||||
enabledByDefault: true,
|
enabledByDefault: true,
|
||||||
|
|
||||||
popAllModals: undefined as (() => void) | undefined,
|
|
||||||
|
|
||||||
settings,
|
settings,
|
||||||
|
|
||||||
patches: [
|
patches: [
|
||||||
@ -62,13 +62,6 @@ export default definePlugin({
|
|||||||
match: /(?=this\.setState\()/,
|
match: /(?=this\.setState\()/,
|
||||||
replace: "$self.handleCrash(this)||"
|
replace: "$self.handleCrash(this)||"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
find: 'dispatch({type:"MODAL_POP_ALL"})',
|
|
||||||
replacement: {
|
|
||||||
match: /"MODAL_POP_ALL".+?};(?<=(\i)=function.+?)/,
|
|
||||||
replace: (m, popAll) => `${m}$self.popAllModals=${popAll};`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
@ -128,7 +121,7 @@ export default definePlugin({
|
|||||||
CrashHandlerLogger.debug("Failed to close open context menu.", err);
|
CrashHandlerLogger.debug("Failed to close open context menu.", err);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
this.popAllModals?.();
|
ModalStack?.popAll();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
CrashHandlerLogger.debug("Failed to close old modals.", err);
|
CrashHandlerLogger.debug("Failed to close old modals.", err);
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { definePluginSettings } from "@api/Settings";
|
import { definePluginSettings, Settings } from "@api/Settings";
|
||||||
import { Link } from "@components/Link";
|
import { Link } from "@components/Link";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { isTruthy } from "@utils/guards";
|
import { isTruthy } from "@utils/guards";
|
||||||
@ -86,8 +86,7 @@ const settings = definePluginSettings({
|
|||||||
appID: {
|
appID: {
|
||||||
type: OptionType.STRING,
|
type: OptionType.STRING,
|
||||||
description: "Application ID (required)",
|
description: "Application ID (required)",
|
||||||
restartNeeded: true,
|
onChange: onChange,
|
||||||
onChange: setRpc,
|
|
||||||
isValid: (value: string) => {
|
isValid: (value: string) => {
|
||||||
if (!value) return "Application ID is required.";
|
if (!value) return "Application ID is required.";
|
||||||
if (value && !/^\d+$/.test(value)) return "Application ID must be a number.";
|
if (value && !/^\d+$/.test(value)) return "Application ID must be a number.";
|
||||||
@ -97,8 +96,7 @@ const settings = definePluginSettings({
|
|||||||
appName: {
|
appName: {
|
||||||
type: OptionType.STRING,
|
type: OptionType.STRING,
|
||||||
description: "Application name (required)",
|
description: "Application name (required)",
|
||||||
restartNeeded: true,
|
onChange: onChange,
|
||||||
onChange: setRpc,
|
|
||||||
isValid: (value: string) => {
|
isValid: (value: string) => {
|
||||||
if (!value) return "Application name is required.";
|
if (!value) return "Application name is required.";
|
||||||
if (value.length > 128) return "Application name must be not longer than 128 characters.";
|
if (value.length > 128) return "Application name must be not longer than 128 characters.";
|
||||||
@ -108,8 +106,7 @@ const settings = definePluginSettings({
|
|||||||
details: {
|
details: {
|
||||||
type: OptionType.STRING,
|
type: OptionType.STRING,
|
||||||
description: "Details (line 1)",
|
description: "Details (line 1)",
|
||||||
restartNeeded: true,
|
onChange: onChange,
|
||||||
onChange: setRpc,
|
|
||||||
isValid: (value: string) => {
|
isValid: (value: string) => {
|
||||||
if (value && value.length > 128) return "Details (line 1) must be not longer than 128 characters.";
|
if (value && value.length > 128) return "Details (line 1) must be not longer than 128 characters.";
|
||||||
return true;
|
return true;
|
||||||
@ -118,8 +115,7 @@ const settings = definePluginSettings({
|
|||||||
state: {
|
state: {
|
||||||
type: OptionType.STRING,
|
type: OptionType.STRING,
|
||||||
description: "State (line 2)",
|
description: "State (line 2)",
|
||||||
restartNeeded: true,
|
onChange: onChange,
|
||||||
onChange: setRpc,
|
|
||||||
isValid: (value: string) => {
|
isValid: (value: string) => {
|
||||||
if (value && value.length > 128) return "State (line 2) must be not longer than 128 characters.";
|
if (value && value.length > 128) return "State (line 2) must be not longer than 128 characters.";
|
||||||
return true;
|
return true;
|
||||||
@ -128,8 +124,7 @@ const settings = definePluginSettings({
|
|||||||
type: {
|
type: {
|
||||||
type: OptionType.SELECT,
|
type: OptionType.SELECT,
|
||||||
description: "Activity type",
|
description: "Activity type",
|
||||||
restartNeeded: true,
|
onChange: onChange,
|
||||||
onChange: setRpc,
|
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
label: "Playing",
|
label: "Playing",
|
||||||
@ -157,16 +152,14 @@ const settings = definePluginSettings({
|
|||||||
streamLink: {
|
streamLink: {
|
||||||
type: OptionType.STRING,
|
type: OptionType.STRING,
|
||||||
description: "Twitch.tv or Youtube.com link (only for Streaming activity type)",
|
description: "Twitch.tv or Youtube.com link (only for Streaming activity type)",
|
||||||
restartNeeded: true,
|
onChange: onChange,
|
||||||
onChange: setRpc,
|
disabled: isStreamLinkDisabled,
|
||||||
isDisabled: isStreamLinkDisabled,
|
|
||||||
isValid: isStreamLinkValid
|
isValid: isStreamLinkValid
|
||||||
},
|
},
|
||||||
timestampMode: {
|
timestampMode: {
|
||||||
type: OptionType.SELECT,
|
type: OptionType.SELECT,
|
||||||
description: "Timestamp mode",
|
description: "Timestamp mode",
|
||||||
restartNeeded: true,
|
onChange: onChange,
|
||||||
onChange: setRpc,
|
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
label: "None",
|
label: "None",
|
||||||
@ -190,9 +183,8 @@ const settings = definePluginSettings({
|
|||||||
startTime: {
|
startTime: {
|
||||||
type: OptionType.NUMBER,
|
type: OptionType.NUMBER,
|
||||||
description: "Start timestamp (only for custom timestamp mode)",
|
description: "Start timestamp (only for custom timestamp mode)",
|
||||||
restartNeeded: true,
|
onChange: onChange,
|
||||||
onChange: setRpc,
|
disabled: isTimestampDisabled,
|
||||||
isDisabled: isTimestampDisabled,
|
|
||||||
isValid: (value: number) => {
|
isValid: (value: number) => {
|
||||||
if (value && value < 0) return "Start timestamp must be greater than 0.";
|
if (value && value < 0) return "Start timestamp must be greater than 0.";
|
||||||
return true;
|
return true;
|
||||||
@ -201,9 +193,8 @@ const settings = definePluginSettings({
|
|||||||
endTime: {
|
endTime: {
|
||||||
type: OptionType.NUMBER,
|
type: OptionType.NUMBER,
|
||||||
description: "End timestamp (only for custom timestamp mode)",
|
description: "End timestamp (only for custom timestamp mode)",
|
||||||
restartNeeded: true,
|
onChange: onChange,
|
||||||
onChange: setRpc,
|
disabled: isTimestampDisabled,
|
||||||
isDisabled: isTimestampDisabled,
|
|
||||||
isValid: (value: number) => {
|
isValid: (value: number) => {
|
||||||
if (value && value < 0) return "End timestamp must be greater than 0.";
|
if (value && value < 0) return "End timestamp must be greater than 0.";
|
||||||
return true;
|
return true;
|
||||||
@ -211,16 +202,14 @@ const settings = definePluginSettings({
|
|||||||
},
|
},
|
||||||
imageBig: {
|
imageBig: {
|
||||||
type: OptionType.STRING,
|
type: OptionType.STRING,
|
||||||
description: "Big image key",
|
description: "Big image key/link",
|
||||||
restartNeeded: true,
|
onChange: onChange,
|
||||||
onChange: setRpc,
|
|
||||||
isValid: isImageKeyValid
|
isValid: isImageKeyValid
|
||||||
},
|
},
|
||||||
imageBigTooltip: {
|
imageBigTooltip: {
|
||||||
type: OptionType.STRING,
|
type: OptionType.STRING,
|
||||||
description: "Big image tooltip",
|
description: "Big image tooltip",
|
||||||
restartNeeded: true,
|
onChange: onChange,
|
||||||
onChange: setRpc,
|
|
||||||
isValid: (value: string) => {
|
isValid: (value: string) => {
|
||||||
if (value && value.length > 128) return "Big image tooltip must be not longer than 128 characters.";
|
if (value && value.length > 128) return "Big image tooltip must be not longer than 128 characters.";
|
||||||
return true;
|
return true;
|
||||||
@ -228,16 +217,14 @@ const settings = definePluginSettings({
|
|||||||
},
|
},
|
||||||
imageSmall: {
|
imageSmall: {
|
||||||
type: OptionType.STRING,
|
type: OptionType.STRING,
|
||||||
description: "Small image key",
|
description: "Small image key/link",
|
||||||
restartNeeded: true,
|
onChange: onChange,
|
||||||
onChange: setRpc,
|
|
||||||
isValid: isImageKeyValid
|
isValid: isImageKeyValid
|
||||||
},
|
},
|
||||||
imageSmallTooltip: {
|
imageSmallTooltip: {
|
||||||
type: OptionType.STRING,
|
type: OptionType.STRING,
|
||||||
description: "Small image tooltip",
|
description: "Small image tooltip",
|
||||||
restartNeeded: true,
|
onChange: onChange,
|
||||||
onChange: setRpc,
|
|
||||||
isValid: (value: string) => {
|
isValid: (value: string) => {
|
||||||
if (value && value.length > 128) return "Small image tooltip must be not longer than 128 characters.";
|
if (value && value.length > 128) return "Small image tooltip must be not longer than 128 characters.";
|
||||||
return true;
|
return true;
|
||||||
@ -246,8 +233,7 @@ const settings = definePluginSettings({
|
|||||||
buttonOneText: {
|
buttonOneText: {
|
||||||
type: OptionType.STRING,
|
type: OptionType.STRING,
|
||||||
description: "Button 1 text",
|
description: "Button 1 text",
|
||||||
restartNeeded: true,
|
onChange: onChange,
|
||||||
onChange: setRpc,
|
|
||||||
isValid: (value: string) => {
|
isValid: (value: string) => {
|
||||||
if (value && value.length > 31) return "Button 1 text must be not longer than 31 characters.";
|
if (value && value.length > 31) return "Button 1 text must be not longer than 31 characters.";
|
||||||
return true;
|
return true;
|
||||||
@ -256,14 +242,12 @@ const settings = definePluginSettings({
|
|||||||
buttonOneURL: {
|
buttonOneURL: {
|
||||||
type: OptionType.STRING,
|
type: OptionType.STRING,
|
||||||
description: "Button 1 URL",
|
description: "Button 1 URL",
|
||||||
restartNeeded: true,
|
onChange: onChange
|
||||||
onChange: setRpc
|
|
||||||
},
|
},
|
||||||
buttonTwoText: {
|
buttonTwoText: {
|
||||||
type: OptionType.STRING,
|
type: OptionType.STRING,
|
||||||
description: "Button 2 text",
|
description: "Button 2 text",
|
||||||
restartNeeded: true,
|
onChange: onChange,
|
||||||
onChange: setRpc,
|
|
||||||
isValid: (value: string) => {
|
isValid: (value: string) => {
|
||||||
if (value && value.length > 31) return "Button 2 text must be not longer than 31 characters.";
|
if (value && value.length > 31) return "Button 2 text must be not longer than 31 characters.";
|
||||||
return true;
|
return true;
|
||||||
@ -272,26 +256,29 @@ const settings = definePluginSettings({
|
|||||||
buttonTwoURL: {
|
buttonTwoURL: {
|
||||||
type: OptionType.STRING,
|
type: OptionType.STRING,
|
||||||
description: "Button 2 URL",
|
description: "Button 2 URL",
|
||||||
restartNeeded: true,
|
onChange: onChange
|
||||||
onChange: setRpc
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function isStreamLinkDisabled(): boolean {
|
function onChange() {
|
||||||
|
setRpc(true);
|
||||||
|
if (Settings.plugins.CustomRPC.enabled) setRpc();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStreamLinkDisabled() {
|
||||||
return settings.store.type !== ActivityType.STREAMING;
|
return settings.store.type !== ActivityType.STREAMING;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isStreamLinkValid(): boolean | string {
|
function isStreamLinkValid(value: string) {
|
||||||
if (settings.store.type === ActivityType.STREAMING && settings.store.streamLink && !/(https?:\/\/(www\.)?(twitch\.tv|youtube\.com)\/\w+)/.test(settings.store.streamLink)) return "Streaming link must be a valid URL.";
|
if (!isStreamLinkDisabled() && !/https?:\/\/(www\.)?(twitch\.tv|youtube\.com)\/\w+/.test(value)) return "Streaming link must be a valid URL.";
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isTimestampDisabled(): boolean {
|
function isTimestampDisabled() {
|
||||||
return settings.store.timestampMode !== TimestampMode.CUSTOM;
|
return settings.store.timestampMode !== TimestampMode.CUSTOM;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isImageKeyValid(value: string) {
|
function isImageKeyValid(value: string) {
|
||||||
if (!/https?:\/\//.test(value)) return true;
|
|
||||||
if (/https?:\/\/(?!i\.)?imgur\.com\//.test(value)) return "Imgur link must be a direct link to the image. (e.g. https://i.imgur.com/...)";
|
if (/https?:\/\/(?!i\.)?imgur\.com\//.test(value)) return "Imgur link must be a direct link to the image. (e.g. https://i.imgur.com/...)";
|
||||||
if (/https?:\/\/(?!media\.)?tenor\.com\//.test(value)) return "Tenor link must be a direct link to the image. (e.g. https://media.tenor.com/...)";
|
if (/https?:\/\/(?!media\.)?tenor\.com\//.test(value)) return "Tenor link must be a direct link to the image. (e.g. https://media.tenor.com/...)";
|
||||||
return true;
|
return true;
|
||||||
@ -342,13 +329,10 @@ async function createActivity(): Promise<Activity | undefined> {
|
|||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
case TimestampMode.CUSTOM:
|
case TimestampMode.CUSTOM:
|
||||||
if (startTime) {
|
if (startTime || endTime) {
|
||||||
activity.timestamps = {
|
activity.timestamps = {};
|
||||||
start: startTime,
|
if (startTime) activity.timestamps.start = startTime;
|
||||||
};
|
if (endTime) activity.timestamps.end = endTime;
|
||||||
if (endTime) {
|
|
||||||
activity.timestamps.end = endTime;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case TimestampMode.NONE:
|
case TimestampMode.NONE:
|
||||||
|
5
src/plugins/dearrow/README.md
Normal file
5
src/plugins/dearrow/README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Dearrow
|
||||||
|
|
||||||
|
Makes YouTube embed titles and thumbnails less sensationalist, powered by [Dearrow](https://dearrow.ajay.app/)
|
||||||
|
|
||||||
|
https://github.com/Vendicated/Vencord/assets/45497981/7bf81108-102d-47c5-8ba5-357db4db1283
|
161
src/plugins/dearrow/index.tsx
Normal file
161
src/plugins/dearrow/index.tsx
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import { Logger } from "@utils/Logger";
|
||||||
|
import definePlugin from "@utils/types";
|
||||||
|
import { Tooltip } from "@webpack/common";
|
||||||
|
import type { Component } from "react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
embed: {
|
||||||
|
rawTitle: string;
|
||||||
|
provider?: {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
thumbnail: {
|
||||||
|
proxyURL: string;
|
||||||
|
};
|
||||||
|
video: {
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
dearrow: {
|
||||||
|
enabled: boolean;
|
||||||
|
oldTitle?: string;
|
||||||
|
oldThumb?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const embedUrlRe = /https:\/\/www\.youtube\.com\/embed\/([a-zA-Z0-9_-]{11})/;
|
||||||
|
|
||||||
|
async function embedDidMount(this: Component<Props>) {
|
||||||
|
try {
|
||||||
|
const { embed } = this.props;
|
||||||
|
if (!embed || embed.dearrow || embed.provider?.name !== "YouTube" || !embed.video?.url) return;
|
||||||
|
|
||||||
|
const videoId = embedUrlRe.exec(embed.video.url)?.[1];
|
||||||
|
if (!videoId) return;
|
||||||
|
|
||||||
|
const res = await fetch(`https://sponsor.ajay.app/api/branding?videoID=${videoId}`);
|
||||||
|
if (!res.ok) return;
|
||||||
|
|
||||||
|
const { titles, thumbnails } = await res.json();
|
||||||
|
|
||||||
|
const hasTitle = titles[0]?.votes >= 0;
|
||||||
|
const hasThumb = thumbnails[0]?.votes >= 0;
|
||||||
|
|
||||||
|
if (!hasTitle && !hasThumb) return;
|
||||||
|
|
||||||
|
embed.dearrow = {
|
||||||
|
enabled: true
|
||||||
|
};
|
||||||
|
|
||||||
|
if (titles[0]?.votes >= 0) {
|
||||||
|
embed.dearrow.oldTitle = embed.rawTitle;
|
||||||
|
embed.rawTitle = titles[0].title;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (thumbnails[0]?.votes >= 0) {
|
||||||
|
embed.dearrow.oldThumb = embed.thumbnail.proxyURL;
|
||||||
|
embed.thumbnail.proxyURL = `https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${thumbnails[0].timestamp}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.forceUpdate();
|
||||||
|
} catch (err) {
|
||||||
|
new Logger("Dearrow").error("Failed to dearrow embed", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function DearrowButton({ component }: { component: Component<Props>; }) {
|
||||||
|
const { embed } = component.props;
|
||||||
|
if (!embed?.dearrow) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip text={embed.dearrow.enabled ? "This embed has been dearrowed, click to restore" : "Click to dearrow"}>
|
||||||
|
{({ onMouseEnter, onMouseLeave }) => (
|
||||||
|
<button
|
||||||
|
onMouseEnter={onMouseEnter}
|
||||||
|
onMouseLeave={onMouseLeave}
|
||||||
|
className={"vc-dearrow-toggle-" + (embed.dearrow.enabled ? "on" : "off")}
|
||||||
|
onClick={() => {
|
||||||
|
const { enabled, oldThumb, oldTitle } = embed.dearrow;
|
||||||
|
embed.dearrow.enabled = !enabled;
|
||||||
|
if (oldTitle) {
|
||||||
|
embed.dearrow.oldTitle = embed.rawTitle;
|
||||||
|
embed.rawTitle = oldTitle;
|
||||||
|
}
|
||||||
|
if (oldThumb) {
|
||||||
|
embed.dearrow.oldThumb = embed.thumbnail.proxyURL;
|
||||||
|
embed.thumbnail.proxyURL = oldThumb;
|
||||||
|
}
|
||||||
|
|
||||||
|
component.forceUpdate();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Dearrow Icon, taken from https://dearrow.ajay.app/logo.svg (and optimised) */}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24px"
|
||||||
|
height="24px"
|
||||||
|
viewBox="0 0 36 36"
|
||||||
|
aria-label="Toggle Dearrow"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="#1213BD"
|
||||||
|
d="M36 18.302c0 4.981-2.46 9.198-5.655 12.462s-7.323 5.152-12.199 5.152s-9.764-1.112-12.959-4.376S0 23.283 0 18.302s2.574-9.38 5.769-12.644S13.271 0 18.146 0s9.394 2.178 12.589 5.442C33.931 8.706 36 13.322 36 18.302z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#88c9f9"
|
||||||
|
d="m 30.394282,18.410186 c 0,3.468849 -1.143025,6.865475 -3.416513,9.137917 -2.273489,2.272442 -5.670115,2.92874 -9.137918,2.92874 -3.467803,0 -6.373515,-1.147212 -8.6470033,-3.419654 -2.2734888,-2.272442 -3.5871299,-5.178154 -3.5871299,-8.647003 0,-3.46885 0.9420533,-6.746149 3.2144954,-9.0196379 2.2724418,-2.2734888 5.5507878,-3.9513905 9.0196378,-3.9513905 3.46885,0 6.492841,1.9322561 8.76633,4.204698 2.273489,2.2724424 3.788101,5.2974804 3.788101,8.7663304 z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#0a62a5"
|
||||||
|
d="m 23.95823,17.818306 c 0,3.153748 -2.644888,5.808102 -5.798635,5.808102 -3.153748,0 -5.599825,-2.654354 -5.599825,-5.808102 0,-3.153747 2.446077,-5.721714 5.599825,-5.721714 3.153747,0 5.798635,2.567967 5.798635,5.721714 z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "Dearrow",
|
||||||
|
description: "Makes YouTube embed titles and thumbnails less sensationalist, powered by Dearrow",
|
||||||
|
authors: [Devs.Ven],
|
||||||
|
|
||||||
|
embedDidMount,
|
||||||
|
renderButton(component: Component<Props>) {
|
||||||
|
return (
|
||||||
|
<ErrorBoundary noop>
|
||||||
|
<DearrowButton component={component} />
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
patches: [{
|
||||||
|
find: "this.renderInlineMediaEmbed",
|
||||||
|
replacement: [
|
||||||
|
// patch componentDidMount to replace embed thumbnail and title
|
||||||
|
{
|
||||||
|
match: /(\i).render=function.{0,50}\i\.embed/,
|
||||||
|
replace: "$1.componentDidMount=$self.embedDidMount,$&"
|
||||||
|
},
|
||||||
|
|
||||||
|
// add dearrow button
|
||||||
|
{
|
||||||
|
match: /children:\[(?=null!=\i\?\i\.renderSuppressButton)/,
|
||||||
|
replace: "children:[$self.renderButton(this),"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}],
|
||||||
|
});
|
12
src/plugins/dearrow/styles.css
Normal file
12
src/plugins/dearrow/styles.css
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
.vc-dearrow-toggle-off svg {
|
||||||
|
filter: grayscale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-dearrow-toggle-on, .vc-dearrow-toggle-off {
|
||||||
|
all: unset;
|
||||||
|
display: inline;
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
top: 0.75rem;
|
||||||
|
right: 0.75rem;
|
||||||
|
}
|
@ -19,7 +19,7 @@
|
|||||||
import { addPreEditListener, addPreSendListener, removePreEditListener, removePreSendListener } from "@api/MessageEvents";
|
import { addPreEditListener, addPreSendListener, removePreEditListener, removePreSendListener } from "@api/MessageEvents";
|
||||||
import { definePluginSettings, Settings } from "@api/Settings";
|
import { definePluginSettings, Settings } from "@api/Settings";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { ApngBlendOp, ApngDisposeOp, getGifEncoder, importApngJs } from "@utils/dependencies";
|
import { ApngBlendOp, ApngDisposeOp, importApngJs } from "@utils/dependencies";
|
||||||
import { getCurrentGuild } from "@utils/discord";
|
import { getCurrentGuild } from "@utils/discord";
|
||||||
import { proxyLazy } from "@utils/lazy";
|
import { proxyLazy } from "@utils/lazy";
|
||||||
import { Logger } from "@utils/Logger";
|
import { Logger } from "@utils/Logger";
|
||||||
@ -27,6 +27,7 @@ import definePlugin, { OptionType } from "@utils/types";
|
|||||||
import { findByCodeLazy, findByPropsLazy, findLazy, findStoreLazy } from "@webpack";
|
import { findByCodeLazy, findByPropsLazy, findLazy, findStoreLazy } from "@webpack";
|
||||||
import { ChannelStore, EmojiStore, FluxDispatcher, Parser, PermissionStore, UserStore } from "@webpack/common";
|
import { ChannelStore, EmojiStore, FluxDispatcher, Parser, PermissionStore, UserStore } from "@webpack/common";
|
||||||
import type { Message } from "discord-types/general";
|
import type { Message } from "discord-types/general";
|
||||||
|
import { applyPalette, GIFEncoder, quantize } from "gifenc";
|
||||||
import type { ReactElement, ReactNode } from "react";
|
import type { ReactElement, ReactNode } from "react";
|
||||||
|
|
||||||
const DRAFT_TYPE = 0;
|
const DRAFT_TYPE = 0;
|
||||||
@ -102,6 +103,11 @@ interface StickerPack {
|
|||||||
stickers: Sticker[];
|
stickers: Sticker[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const enum FakeNoticeType {
|
||||||
|
Sticker,
|
||||||
|
Emoji
|
||||||
|
}
|
||||||
|
|
||||||
const fakeNitroEmojiRegex = /\/emojis\/(\d+?)\.(png|webp|gif)/;
|
const fakeNitroEmojiRegex = /\/emojis\/(\d+?)\.(png|webp|gif)/;
|
||||||
const fakeNitroStickerRegex = /\/stickers\/(\d+?)\./;
|
const fakeNitroStickerRegex = /\/stickers\/(\d+?)\./;
|
||||||
const fakeNitroGifStickerRegex = /\/attachments\/\d+?\/\d+?\/(\d+?)\.gif/;
|
const fakeNitroGifStickerRegex = /\/attachments\/\d+?\/\d+?\/(\d+?)\.gif/;
|
||||||
@ -212,15 +218,15 @@ export default definePlugin({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: "canStreamHighQuality:function",
|
find: "canUseHighVideoUploadQuality:function",
|
||||||
predicate: () => settings.store.enableStreamQualityBypass,
|
predicate: () => settings.store.enableStreamQualityBypass,
|
||||||
replacement: [
|
replacement: [
|
||||||
"canUseHighVideoUploadQuality",
|
"canUseHighVideoUploadQuality",
|
||||||
"canStreamHighQuality",
|
// TODO: Remove the last two when they get removed from stable
|
||||||
"canStreamMidQuality"
|
"(?:canStreamQuality|canStreamHighQuality|canStreamMidQuality)",
|
||||||
].map(func => {
|
].map(func => {
|
||||||
return {
|
return {
|
||||||
match: new RegExp(`${func}:function\\(\\i\\){`),
|
match: new RegExp(`${func}:function\\(\\i(?:,\\i)?\\){`, "g"),
|
||||||
replace: "$&return true;"
|
replace: "$&return true;"
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@ -305,16 +311,24 @@ export default definePlugin({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /(emojiSection.{0,50}description:)(\i)(?<=(\i)\.sticker,.+?)(?=,)/,
|
match: /(emojiSection.{0,50}description:)(\i)(?<=(\i)\.sticker,.+?)(?=,)/,
|
||||||
replace: (_, rest, reactNode, props) => `${rest}$self.addFakeNotice("STICKER",${reactNode},!!${props}.renderableSticker?.fake)`
|
replace: (_, rest, reactNode, props) => `${rest}$self.addFakeNotice(${FakeNoticeType.Sticker},${reactNode},!!${props}.renderableSticker?.fake)`
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: ".Messages.EMOJI_POPOUT_PREMIUM_JOINED_GUILD_DESCRIPTION",
|
find: ".EMOJI_UPSELL_POPOUT_MORE_EMOJIS_OPENED,",
|
||||||
predicate: () => settings.store.transformEmojis,
|
predicate: () => settings.store.transformEmojis,
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /((\i)=\i\.node,\i=\i\.emojiSourceDiscoverableGuild)(.+?return )(.{0,450}Messages\.EMOJI_POPOUT_PREMIUM_JOINED_GUILD_DESCRIPTION.+?}\))/,
|
match: /isDiscoverable:\i,shouldHideRoleSubscriptionCTA:\i,(?<=(\i)=\i\.node.+?)/,
|
||||||
replace: (_, rest1, node, rest2, reactNode) => `${rest1},fakeNitroNode=${node}${rest2}$self.addFakeNotice("EMOJI",${reactNode},fakeNitroNode.fake)`
|
replace: (m, node) => `${m}fakeNitroNode:${node},`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: ".Messages.EMOJI_POPOUT_UNJOINED_DISCOVERABLE_GUILD_DESCRIPTION",
|
||||||
|
predicate: () => settings.store.transformEmojis,
|
||||||
|
replacement: {
|
||||||
|
match: /(?<=\.Messages\.EMOJI_POPOUT_ADDED_PACK_DESCRIPTION.+?return )(.{0,1200}\.Messages\.EMOJI_POPOUT_UNJOINED_DISCOVERABLE_GUILD_DESCRIPTION.+?)(}\({)/,
|
||||||
|
replace: (_, reactNode, rest) => `$self.addFakeNotice(${FakeNoticeType.Emoji},${reactNode},!!arguments[0]?.fakeNitroNode?.fake)${rest}fakeNitroNode:arguments[0]?.fakeNitroNode,`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -610,18 +624,18 @@ export default definePlugin({
|
|||||||
return link.target && fakeNitroEmojiRegex.test(link.target);
|
return link.target && fakeNitroEmojiRegex.test(link.target);
|
||||||
},
|
},
|
||||||
|
|
||||||
addFakeNotice(type: "STICKER" | "EMOJI", node: Array<ReactNode>, fake: boolean) {
|
addFakeNotice(type: FakeNoticeType, node: Array<ReactNode>, fake: boolean) {
|
||||||
if (!fake) return node;
|
if (!fake) return node;
|
||||||
|
|
||||||
node = Array.isArray(node) ? node : [node];
|
node = Array.isArray(node) ? node : [node];
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "STICKER": {
|
case FakeNoticeType.Sticker: {
|
||||||
node.push(" This is a FakeNitro sticker and renders like a real sticker only for you. Appears as a link to non-plugin users.");
|
node.push(" This is a FakeNitro sticker and renders like a real sticker only for you. Appears as a link to non-plugin users.");
|
||||||
|
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
case "EMOJI": {
|
case FakeNoticeType.Emoji: {
|
||||||
node.push(" This is a FakeNitro emoji and renders like a real emoji only for you. Appears as a link to non-plugin users.");
|
node.push(" This is a FakeNitro emoji and renders like a real emoji only for you. Appears as a link to non-plugin users.");
|
||||||
|
|
||||||
return node;
|
return node;
|
||||||
@ -650,15 +664,11 @@ export default definePlugin({
|
|||||||
},
|
},
|
||||||
|
|
||||||
async sendAnimatedSticker(stickerLink: string, stickerId: string, channelId: string) {
|
async sendAnimatedSticker(stickerLink: string, stickerId: string, channelId: string) {
|
||||||
const [{ parseURL }, {
|
const { parseURL } = importApngJs();
|
||||||
GIFEncoder,
|
|
||||||
quantize,
|
|
||||||
applyPalette
|
|
||||||
}] = await Promise.all([importApngJs(), getGifEncoder()]);
|
|
||||||
|
|
||||||
const { frames, width, height } = await parseURL(stickerLink);
|
const { frames, width, height } = await parseURL(stickerLink);
|
||||||
|
|
||||||
const gif = new GIFEncoder();
|
const gif = GIFEncoder();
|
||||||
const resolution = Settings.plugins.FakeNitro.stickerSize;
|
const resolution = Settings.plugins.FakeNitro.stickerSize;
|
||||||
|
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
|
6
src/plugins/favEmojiFirst/README.md
Normal file
6
src/plugins/favEmojiFirst/README.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# FavoriteEmojiFirst
|
||||||
|
|
||||||
|
Puts your favorite emoji first in the emoji autocomplete.
|
||||||
|
|
||||||
|
![FavEmojis](https://i.imgur.com/mEFCoZG.png)
|
||||||
|
![Example](https://i.imgur.com/wY3Tc43.png)
|
5
src/plugins/favGifSearch/README.md
Normal file
5
src/plugins/favGifSearch/README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# FavoriteGifSearch
|
||||||
|
|
||||||
|
Adds a search bar to favorite gifs.
|
||||||
|
|
||||||
|
![Screenshot](https://i.imgur.com/Bcgb7PD.png)
|
@ -60,7 +60,7 @@ interface Instance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const containerClasses: { searchBar: string; } = findByPropsLazy("searchBar", "searchHeader", "gutterSize");
|
const containerClasses: { searchBar: string; } = findByPropsLazy("searchBar", "searchHeader", "searchInput");
|
||||||
|
|
||||||
export const settings = definePluginSettings({
|
export const settings = definePluginSettings({
|
||||||
searchOption: {
|
searchOption: {
|
||||||
@ -87,7 +87,7 @@ export const settings = definePluginSettings({
|
|||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "FavoriteGifSearch",
|
name: "FavoriteGifSearch",
|
||||||
authors: [Devs.Aria],
|
authors: [Devs.Aria],
|
||||||
description: "Adds a search bar for favorite gifs",
|
description: "Adds a search bar to favorite gifs.",
|
||||||
|
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
@ -1,56 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2023 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Devs } from "@utils/constants";
|
|
||||||
import definePlugin from "@utils/types";
|
|
||||||
import { Forms } from "@webpack/common";
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "FixInbox",
|
|
||||||
description: "Fixes the Unreads Inbox from crashing Discord when you're in lots of guilds.",
|
|
||||||
authors: [Devs.Megu],
|
|
||||||
|
|
||||||
patches: [{
|
|
||||||
find: "INBOX_OPEN:function",
|
|
||||||
replacement: {
|
|
||||||
// This function normally dispatches a subscribe event to every guild.
|
|
||||||
// this is badbadbadbadbad so we just get rid of it.
|
|
||||||
match: /INBOX_OPEN:function.+?\{/,
|
|
||||||
replace: "$&return true;"
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
|
|
||||||
settingsAboutComponent() {
|
|
||||||
return (
|
|
||||||
<Forms.FormSection>
|
|
||||||
<Forms.FormTitle tag="h3">What's the problem?</Forms.FormTitle>
|
|
||||||
<Forms.FormText style={{ marginBottom: 8 }}>
|
|
||||||
By default, Discord emits a GUILD_SUBSCRIPTIONS event for every guild you're in.
|
|
||||||
When you're in a lot of guilds, this can cause the gateway to ratelimit you.
|
|
||||||
This causes the client to crash and get stuck in an infinite ratelimit loop as it tries to reconnect.
|
|
||||||
</Forms.FormText>
|
|
||||||
|
|
||||||
<Forms.FormTitle tag="h3">How does it work?</Forms.FormTitle>
|
|
||||||
<Forms.FormText>
|
|
||||||
This plugin works by stopping the client from sending GUILD_SUBSCRIPTIONS events to the gateway when you open the unreads inbox.
|
|
||||||
This means that not all unreads will be shown, instead only already-subscribed guilds' unreads will be shown, but your client won't crash anymore.
|
|
||||||
</Forms.FormText>
|
|
||||||
</Forms.FormSection>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
26
src/plugins/fixSpotifyEmbeds.desktop.ts
Normal file
26
src/plugins/fixSpotifyEmbeds.desktop.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { definePluginSettings } from "@api/Settings";
|
||||||
|
import { makeRange } from "@components/PluginSettings/components";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
|
|
||||||
|
// The entire code of this plugin can be found in ipcPlugins
|
||||||
|
export default definePlugin({
|
||||||
|
name: "FixSpotifyEmbeds",
|
||||||
|
description: "Fixes spotify embeds being incredibly loud by letting you customise the volume",
|
||||||
|
authors: [Devs.Ven],
|
||||||
|
settings: definePluginSettings({
|
||||||
|
volume: {
|
||||||
|
type: OptionType.SLIDER,
|
||||||
|
description: "The volume % to set for spotify embeds. Anything above 10% is veeeery loud",
|
||||||
|
markers: makeRange(0, 100, 10),
|
||||||
|
stickToMarkers: false,
|
||||||
|
default: 10
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
@ -32,12 +32,18 @@ function makeIcon(showCurrentGame?: boolean) {
|
|||||||
return function () {
|
return function () {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
width="24"
|
width="20"
|
||||||
height="24"
|
height="20"
|
||||||
viewBox="0 96 960 960"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<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" />
|
<path fill="currentColor" mask="url(#gameActivityMask)" d="M3.06 20.4q-1.53 0-2.37-1.065T.06 16.74l1.26-9q.27-1.8 1.605-2.97T6.06 3.6h11.88q1.8 0 3.135 1.17t1.605 2.97l1.26 9q.21 1.53-.63 2.595T20.94 20.4q-.63 0-1.17-.225T18.78 19.5l-2.7-2.7H7.92l-2.7 2.7q-.45.45-.99.675t-1.17.225Zm14.94-7.2q.51 0 .855-.345T19.2 12q0-.51-.345-.855T18 10.8q-.51 0-.855.345T16.8 12q0 .51.345 .855T18 13.2Zm-2.4-3.6q.51 0 .855-.345T16.8 8.4q0-.51-.345-.855T15.6 7.2q-.51 0-.855.345T14.4 8.4q0 .51.345 .855T15.6 9.6ZM6.9 13.2h1.8v-2.1h2.1v-1.8h-2.1v-2.1h-1.8v2.1h-2.1v1.8h2.1v2.1Z" />
|
||||||
{!showCurrentGame && <line x1="920" y1="280" x2="40" y2="880" stroke="var(--status-danger)" stroke-width="80" />}
|
{!showCurrentGame && <>
|
||||||
|
<mask id="gameActivityMask" >
|
||||||
|
<rect fill="white" x="0" y="0" width="24" height="24" />
|
||||||
|
<path fill="black" d="M23.27 4.54 19.46.73 .73 19.46 4.54 23.27 23.27 4.54Z"/>
|
||||||
|
</mask>
|
||||||
|
<path fill="var(--status-danger)" d="M23 2.27 21.73 1 1 21.73 2.27 23 23 2.27Z" />
|
||||||
|
</>}
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -60,7 +66,7 @@ function GameActivityToggleButton() {
|
|||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "GameActivityToggle",
|
name: "GameActivityToggle",
|
||||||
description: "Adds a button next to the mic and deafen button to toggle game activity.",
|
description: "Adds a button next to the mic and deafen button to toggle game activity.",
|
||||||
authors: [Devs.Nuckyz],
|
authors: [Devs.Nuckyz, Devs.RuukuLada],
|
||||||
dependencies: ["SettingsStoreAPI"],
|
dependencies: ["SettingsStoreAPI"],
|
||||||
|
|
||||||
patches: [
|
patches: [
|
||||||
|
@ -49,7 +49,7 @@ export default definePlugin({
|
|||||||
await this.buildCss();
|
await this.buildCss();
|
||||||
|
|
||||||
addButton("HideAttachments", msg => {
|
addButton("HideAttachments", msg => {
|
||||||
if (!msg.attachments.length && !msg.embeds.length) return null;
|
if (!msg.attachments.length && !msg.embeds.length && !msg.stickerItems.length) return null;
|
||||||
|
|
||||||
const isHidden = hiddenMessages.has(msg.id);
|
const isHidden = hiddenMessages.has(msg.id);
|
||||||
|
|
||||||
@ -72,7 +72,7 @@ export default definePlugin({
|
|||||||
async buildCss() {
|
async buildCss() {
|
||||||
const elements = [...hiddenMessages].map(id => `#message-accessories-${id}`).join(",");
|
const elements = [...hiddenMessages].map(id => `#message-accessories-${id}`).join(",");
|
||||||
style.textContent = `
|
style.textContent = `
|
||||||
:is(${elements}) [class*="embedWrapper"] {
|
:is(${elements}) :is([class*="embedWrapper"], [class*="clickableSticker"]) {
|
||||||
/* important is not necessary, but add it to make sure bad themes won't break it */
|
/* important is not necessary, but add it to make sure bad themes won't break it */
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
6
src/plugins/imageZoom/README.md
Normal file
6
src/plugins/imageZoom/README.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# ImageZoom
|
||||||
|
|
||||||
|
Lets you zoom in to images and gifs. Use scroll wheel to zoom in and shift + scroll wheel to increase lens radius / size
|
||||||
|
|
||||||
|
![Example](https://i.imgur.com/VJdo4aq.png)
|
||||||
|
![ContextMenu](https://i.imgur.com/0oaRM2s.png)
|
@ -99,6 +99,15 @@ const imageContextMenuPatch: NavContextMenuPatchCallback = children => () => {
|
|||||||
ContextMenu.close();
|
ContextMenu.close();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Menu.MenuCheckboxItem
|
||||||
|
id="vc-nearest-neighbour"
|
||||||
|
label="Nearset Neighbour"
|
||||||
|
checked={settings.store.nearestNeighbour}
|
||||||
|
action={() => {
|
||||||
|
settings.store.nearestNeighbour = !settings.store.nearestNeighbour;
|
||||||
|
ContextMenu.close();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Menu.MenuControlItem
|
<Menu.MenuControlItem
|
||||||
id="vc-zoom"
|
id="vc-zoom"
|
||||||
label="Zoom"
|
label="Zoom"
|
||||||
|
@ -28,7 +28,7 @@ import { Button, Forms, React, TextInput } from "@webpack/common";
|
|||||||
import { decrypt } from "../index";
|
import { decrypt } from "../index";
|
||||||
|
|
||||||
export function DecModal(props: any) {
|
export function DecModal(props: any) {
|
||||||
const secret: string = props?.message?.content;
|
const encryptedMessage: string = props?.message?.content;
|
||||||
const [password, setPassword] = React.useState("password");
|
const [password, setPassword] = React.useState("password");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -38,9 +38,9 @@ export function DecModal(props: any) {
|
|||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Secret</Forms.FormTitle>
|
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Message with Encryption</Forms.FormTitle>
|
||||||
<TextInput defaultValue={secret} disabled={true}></TextInput>
|
<TextInput defaultValue={encryptedMessage} disabled={true}></TextInput>
|
||||||
<Forms.FormTitle tag="h5">Password</Forms.FormTitle>
|
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Password</Forms.FormTitle>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={{ marginBottom: "20px" }}
|
style={{ marginBottom: "20px" }}
|
||||||
onChange={setPassword}
|
onChange={setPassword}
|
||||||
@ -51,7 +51,7 @@ export function DecModal(props: any) {
|
|||||||
<Button
|
<Button
|
||||||
color={Button.Colors.GREEN}
|
color={Button.Colors.GREEN}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const toSend = decrypt(secret, password, true);
|
const toSend = decrypt(encryptedMessage, password, true);
|
||||||
if (!toSend || !props?.message) return;
|
if (!toSend || !props?.message) return;
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
Vencord.Plugins.plugins.InvisibleChat.buildEmbed(props?.message, toSend);
|
Vencord.Plugins.plugins.InvisibleChat.buildEmbed(props?.message, toSend);
|
@ -35,7 +35,7 @@ function EncModal(props: ModalProps) {
|
|||||||
const [password, setPassword] = React.useState("password");
|
const [password, setPassword] = React.useState("password");
|
||||||
const [noCover, setNoCover] = React.useState(false);
|
const [noCover, setNoCover] = React.useState(false);
|
||||||
|
|
||||||
const isValid = secret && (noCover || (cover && /\w \w/.test(cover)));
|
const isValid = secret && (noCover || (cover && cover.trim().split(" ").length > 1));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalRoot {...props}>
|
<ModalRoot {...props}>
|
@ -225,8 +225,8 @@ export function encrypt(secret: string, password: string, cover: string): string
|
|||||||
return steggo.hide(secret + "\u200b", password, cover);
|
return steggo.hide(secret + "\u200b", password, cover);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function decrypt(secret: string, password: string, removeIndicator: boolean): string {
|
export function decrypt(encrypted: string, password: string, removeIndicator: boolean): string {
|
||||||
const decrypted = steggo.reveal(secret, password);
|
const decrypted = steggo.reveal(encrypted, password);
|
||||||
return removeIndicator ? decrypted.replace("\u200b", "") : decrypted;
|
return removeIndicator ? decrypted.replace("\u200b", "") : decrypted;
|
||||||
}
|
}
|
||||||
|
|
@ -72,6 +72,14 @@ const enum ActivityFlag {
|
|||||||
INSTANCE = 1 << 0,
|
INSTANCE = 1 << 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const enum NameFormat {
|
||||||
|
StatusName = "status-name",
|
||||||
|
ArtistFirst = "artist-first",
|
||||||
|
SongFirst = "song-first",
|
||||||
|
ArtistOnly = "artist",
|
||||||
|
SongOnly = "song"
|
||||||
|
}
|
||||||
|
|
||||||
const applicationId = "1108588077900898414";
|
const applicationId = "1108588077900898414";
|
||||||
const placeholderId = "2a96cbd8b46e442fc41c2b86b821562f";
|
const placeholderId = "2a96cbd8b46e442fc41c2b86b821562f";
|
||||||
|
|
||||||
@ -117,10 +125,37 @@ const settings = definePluginSettings({
|
|||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
statusName: {
|
statusName: {
|
||||||
description: "text shown in status",
|
description: "custom status text",
|
||||||
type: OptionType.STRING,
|
type: OptionType.STRING,
|
||||||
default: "some music",
|
default: "some music",
|
||||||
},
|
},
|
||||||
|
nameFormat: {
|
||||||
|
description: "Show name of song and artist in status name",
|
||||||
|
type: OptionType.SELECT,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: "Use custom status name",
|
||||||
|
value: NameFormat.StatusName,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Use format 'artist - song'",
|
||||||
|
value: NameFormat.ArtistFirst
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Use format 'song - artist'",
|
||||||
|
value: NameFormat.SongFirst
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Use artist name only",
|
||||||
|
value: NameFormat.ArtistOnly
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Use song name only",
|
||||||
|
value: NameFormat.SongOnly
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
useListeningStatus: {
|
useListeningStatus: {
|
||||||
description: 'show "Listening to" status instead of "Playing"',
|
description: 'show "Listening to" status instead of "Playing"',
|
||||||
type: OptionType.BOOLEAN,
|
type: OptionType.BOOLEAN,
|
||||||
@ -140,13 +175,13 @@ const settings = definePluginSettings({
|
|||||||
value: "placeholder"
|
value: "placeholder"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "LastFMRichPresence",
|
name: "LastFMRichPresence",
|
||||||
description: "Little plugin for Last.fm rich presence",
|
description: "Little plugin for Last.fm rich presence",
|
||||||
authors: [Devs.dzshn, Devs.RuiNtD],
|
authors: [Devs.dzshn, Devs.RuiNtD, Devs.blahajZip, Devs.archeruwu],
|
||||||
|
|
||||||
settingsAboutComponent: () => (
|
settingsAboutComponent: () => (
|
||||||
<>
|
<>
|
||||||
@ -267,9 +302,24 @@ export default definePlugin({
|
|||||||
url: `https://www.last.fm/user/${settings.store.username}`,
|
url: `https://www.last.fm/user/${settings.store.username}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const statusName = (() => {
|
||||||
|
switch (settings.store.nameFormat) {
|
||||||
|
case NameFormat.ArtistFirst:
|
||||||
|
return trackData.artist + " - " + trackData.name;
|
||||||
|
case NameFormat.SongFirst:
|
||||||
|
return trackData.name + " - " + trackData.artist;
|
||||||
|
case NameFormat.ArtistOnly:
|
||||||
|
return trackData.artist;
|
||||||
|
case NameFormat.SongOnly:
|
||||||
|
return trackData.name;
|
||||||
|
default:
|
||||||
|
return settings.store.statusName;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
application_id: applicationId,
|
application_id: applicationId,
|
||||||
name: settings.store.statusName,
|
name: statusName,
|
||||||
|
|
||||||
details: trackData.name,
|
details: trackData.name,
|
||||||
state: trackData.artist,
|
state: trackData.artist,
|
||||||
|
@ -56,7 +56,8 @@ const quotes = [
|
|||||||
"hd{b${",
|
"hd{b${",
|
||||||
"<;vqkijbq33271:56<3799?24944:",
|
"<;vqkijbq33271:56<3799?24944:",
|
||||||
"Thof$lu'ofdn,!qsefc'az*bnrcma+&Om{o+iu\"`khct$)bnrd\"bcdoi&",
|
"Thof$lu'ofdn,!qsefc'az*bnrcma+&Om{o+iu\"`khct$)bnrd\"bcdoi&",
|
||||||
"snofplkb{)c'r\"lod'|f*aurv#cpno`abchijklmno"
|
"snofplkb{)c'r\"lod'|f*aurv#cpno`abchijklmno",
|
||||||
|
"Wdn`khc'|f*eghl{%"
|
||||||
];
|
];
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
|
@ -30,6 +30,9 @@ const ChannelMemberStore = findStoreLazy("ChannelMemberStore") as FluxStore & {
|
|||||||
getProps(guildId: string, channelId: string): { groups: { count: number; id: string; }[]; };
|
getProps(guildId: string, channelId: string): { groups: { count: number; id: string; }[]; };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sharedIntlNumberFormat = new Intl.NumberFormat();
|
||||||
|
const numberFormat = (value: number) => sharedIntlNumberFormat.format(value);
|
||||||
|
|
||||||
function MemberCount() {
|
function MemberCount() {
|
||||||
const { id: channelId, guild_id: guildId } = useStateFromStores([SelectedChannelStore], () => getCurrentChannel());
|
const { id: channelId, guild_id: guildId } = useStateFromStores([SelectedChannelStore], () => getCurrentChannel());
|
||||||
const { groups } = useStateFromStores(
|
const { groups } = useStateFromStores(
|
||||||
@ -57,7 +60,7 @@ function MemberCount() {
|
|||||||
alignContent: "center",
|
alignContent: "center",
|
||||||
gap: 0
|
gap: 0
|
||||||
}}>
|
}}>
|
||||||
<Tooltip text={`${online} Online in this Channel`} position="bottom">
|
<Tooltip text={`${numberFormat(online)} online in this channel`} position="bottom">
|
||||||
{props => (
|
{props => (
|
||||||
<div {...props}>
|
<div {...props}>
|
||||||
<span
|
<span
|
||||||
@ -70,11 +73,11 @@ function MemberCount() {
|
|||||||
marginRight: "0.5em"
|
marginRight: "0.5em"
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span style={{ color: "var(--green-360)" }}>{online}</span>
|
<span style={{ color: "var(--green-360)" }}>{numberFormat(online)}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip text={`${total} Total Server Members`} position="bottom">
|
<Tooltip text={`${numberFormat(total)} total server members`} position="bottom">
|
||||||
{props => (
|
{props => (
|
||||||
<div {...props}>
|
<div {...props}>
|
||||||
<span
|
<span
|
||||||
@ -88,7 +91,7 @@ function MemberCount() {
|
|||||||
marginLeft: "1em"
|
marginLeft: "1em"
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span style={{ color: "var(--primary-400)" }}>{total}</span>
|
<span style={{ color: "var(--primary-400)" }}>{numberFormat(total)}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
import { addAccessory } from "@api/MessageAccessories";
|
import { addAccessory } from "@api/MessageAccessories";
|
||||||
import { definePluginSettings } from "@api/Settings";
|
import { definePluginSettings } from "@api/Settings";
|
||||||
|
import { getSettingStoreLazy } from "@api/SettingsStore";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Devs } from "@utils/constants.js";
|
import { Devs } from "@utils/constants.js";
|
||||||
import { classes } from "@utils/misc";
|
import { classes } from "@utils/misc";
|
||||||
@ -318,9 +319,10 @@ function ChannelMessageEmbedAccessory({ message, channel, guildID }: MessageEmbe
|
|||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const compactModeEnabled = getSettingStoreLazy<boolean>("textAndImages", "messageDisplayCompact")!;
|
||||||
|
|
||||||
function AutomodEmbedAccessory(props: MessageEmbedProps): JSX.Element | null {
|
function AutomodEmbedAccessory(props: MessageEmbedProps): JSX.Element | null {
|
||||||
const { message, channel, guildID } = props;
|
const { message, channel, guildID } = props;
|
||||||
|
|
||||||
const isDM = guildID === "@me";
|
const isDM = guildID === "@me";
|
||||||
const images = getImages(message);
|
const images = getImages(message);
|
||||||
const { parse } = Parser;
|
const { parse } = Parser;
|
||||||
@ -336,7 +338,7 @@ function AutomodEmbedAccessory(props: MessageEmbedProps): JSX.Element | null {
|
|||||||
<span>{isDM ? " - Direct Message" : " - " + GuildStore.getGuild(channel.guild_id)?.name}</span>
|
<span>{isDM ? " - Direct Message" : " - " + GuildStore.getGuild(channel.guild_id)?.name}</span>
|
||||||
</Text>
|
</Text>
|
||||||
}
|
}
|
||||||
compact={false}
|
compact={compactModeEnabled.getSetting()}
|
||||||
content={
|
content={
|
||||||
<>
|
<>
|
||||||
{message.content || message.attachments.length <= images.length
|
{message.content || message.attachments.length <= images.length
|
||||||
@ -363,7 +365,7 @@ export default definePlugin({
|
|||||||
name: "MessageLinkEmbeds",
|
name: "MessageLinkEmbeds",
|
||||||
description: "Adds a preview to messages that link another message",
|
description: "Adds a preview to messages that link another message",
|
||||||
authors: [Devs.TheSun, Devs.Ven, Devs.RyanCaoDev],
|
authors: [Devs.TheSun, Devs.Ven, Devs.RyanCaoDev],
|
||||||
dependencies: ["MessageAccessoriesAPI"],
|
dependencies: ["MessageAccessoriesAPI", "SettingsStoreAPI"],
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: ".embedCard",
|
find: ".embedCard",
|
||||||
|
@ -3,6 +3,11 @@
|
|||||||
color: #f04747 !important;
|
color: #f04747 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Bot "thinking" text highlighting */
|
||||||
|
.messagelogger-deleted [class*="colorStandard-"] {
|
||||||
|
color: #f04747 !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Embed highlighting */
|
/* Embed highlighting */
|
||||||
.messagelogger-deleted article :is(div, span, h1, h2, h3, p) {
|
.messagelogger-deleted article :is(div, span, h1, h2, h3, p) {
|
||||||
color: #f04747 !important;
|
color: #f04747 !important;
|
||||||
|
@ -169,21 +169,14 @@ export default definePlugin({
|
|||||||
try {
|
try {
|
||||||
if (cache == null || (!isBulk && !cache.has(data.id))) return cache;
|
if (cache == null || (!isBulk && !cache.has(data.id))) return cache;
|
||||||
|
|
||||||
const { ignoreBots, ignoreSelf, ignoreUsers, ignoreChannels, ignoreGuilds } = Settings.plugins.MessageLogger;
|
const mutate = (id: string) => {
|
||||||
const myId = UserStore.getCurrentUser().id;
|
|
||||||
|
|
||||||
function mutate(id: string) {
|
|
||||||
const msg = cache.get(id);
|
const msg = cache.get(id);
|
||||||
if (!msg) return;
|
if (!msg) return;
|
||||||
|
|
||||||
const EPHEMERAL = 64;
|
const EPHEMERAL = 64;
|
||||||
const shouldIgnore = data.mlDeleted ||
|
const shouldIgnore = data.mlDeleted ||
|
||||||
(msg.flags & EPHEMERAL) === EPHEMERAL ||
|
(msg.flags & EPHEMERAL) === EPHEMERAL ||
|
||||||
ignoreBots && msg.author?.bot ||
|
this.shouldIgnore(msg);
|
||||||
ignoreSelf && msg.author?.id === myId ||
|
|
||||||
ignoreUsers.includes(msg.author?.id) ||
|
|
||||||
ignoreChannels.includes(msg.channel_id) ||
|
|
||||||
ignoreGuilds.includes(ChannelStore.getChannel(msg.channel_id)?.guild_id);
|
|
||||||
|
|
||||||
if (shouldIgnore) {
|
if (shouldIgnore) {
|
||||||
cache = cache.remove(id);
|
cache = cache.remove(id);
|
||||||
@ -192,7 +185,7 @@ export default definePlugin({
|
|||||||
.set("deleted", true)
|
.set("deleted", true)
|
||||||
.set("attachments", m.attachments.map(a => (a.deleted = true, a))));
|
.set("attachments", m.attachments.map(a => (a.deleted = true, a))));
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
if (isBulk) {
|
if (isBulk) {
|
||||||
data.ids.forEach(mutate);
|
data.ids.forEach(mutate);
|
||||||
@ -205,6 +198,18 @@ export default definePlugin({
|
|||||||
return cache;
|
return cache;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
shouldIgnore(message: any) {
|
||||||
|
const { ignoreBots, ignoreSelf, ignoreUsers, ignoreChannels, ignoreGuilds } = Settings.plugins.MessageLogger;
|
||||||
|
const myId = UserStore.getCurrentUser().id;
|
||||||
|
|
||||||
|
return ignoreBots && message.author?.bot ||
|
||||||
|
ignoreSelf && message.author?.id === myId ||
|
||||||
|
ignoreUsers.includes(message.author?.id) ||
|
||||||
|
ignoreChannels.includes(message.channel_id) ||
|
||||||
|
ignoreChannels.includes(ChannelStore.getChannel(message.channel_id)?.parent_id) ||
|
||||||
|
ignoreGuilds.includes(ChannelStore.getChannel(message.channel_id)?.guild_id);
|
||||||
|
},
|
||||||
|
|
||||||
// Based on canary 9ab8626bcebceaea6da570b9c586172d02b9c996
|
// Based on canary 9ab8626bcebceaea6da570b9c586172d02b9c996
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
@ -237,7 +242,7 @@ export default definePlugin({
|
|||||||
match: /(MESSAGE_UPDATE:function\((\w)\).+?)\.update\((\w)/,
|
match: /(MESSAGE_UPDATE:function\((\w)\).+?)\.update\((\w)/,
|
||||||
replace: "$1" +
|
replace: "$1" +
|
||||||
".update($3,m =>" +
|
".update($3,m =>" +
|
||||||
" (($2.message.flags & 64) === 64 || (Vencord.Settings.plugins.MessageLogger.ignoreBots && $2.message.author?.bot) || (Vencord.Settings.plugins.MessageLogger.ignoreSelf && $2.message.author?.id === Vencord.Webpack.Common.UserStore.getCurrentUser().id)) ? m :" +
|
" (($2.message.flags & 64) === 64 || $self.shouldIgnore($2.message)) ? m :" +
|
||||||
" $2.message.content !== m.editHistory?.[0]?.content && $2.message.content !== m.content ?" +
|
" $2.message.content !== m.editHistory?.[0]?.content && $2.message.content !== m.content ?" +
|
||||||
" m.set('editHistory',[...(m.editHistory || []), $self.makeEdit($2.message, m)]) :" +
|
" m.set('editHistory',[...(m.editHistory || []), $self.makeEdit($2.message, m)]) :" +
|
||||||
" m" +
|
" m" +
|
||||||
@ -321,14 +326,14 @@ export default definePlugin({
|
|||||||
{
|
{
|
||||||
// Attachment renderer
|
// Attachment renderer
|
||||||
// Module 96063
|
// Module 96063
|
||||||
find: "[\"className\",\"attachment\",\"inlineMedia\"",
|
find: "().removeAttachmentHoverButton",
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /((\w)\.className,\w=\2\.attachment),/,
|
match: /((\w)\.className,\w=\2\.attachment),/,
|
||||||
replace: "$1,deleted=$2.attachment?.deleted,"
|
replace: "$1,deleted=$2.attachment?.deleted,"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /\["className","attachment","inlineMedia".+?className:/,
|
match: /\["className","attachment".+?className:/,
|
||||||
replace: "$& (deleted ? 'messagelogger-deleted-attachment ' : '') +"
|
replace: "$& (deleted ? 'messagelogger-deleted-attachment ' : '') +"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -185,7 +185,7 @@ const settings = definePluginSettings({
|
|||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "MoreUserTags",
|
name: "MoreUserTags",
|
||||||
description: "Adds tags for webhooks and moderative roles (owner, admin, etc.)",
|
description: "Adds tags for webhooks and moderative roles (owner, admin, etc.)",
|
||||||
authors: [Devs.Cyn, Devs.TheSun, Devs.RyanCaoDev, Devs.LordElias],
|
authors: [Devs.Cyn, Devs.TheSun, Devs.RyanCaoDev, Devs.LordElias, Devs.AutumnVN],
|
||||||
settings,
|
settings,
|
||||||
patches: [
|
patches: [
|
||||||
// add tags to the tag list
|
// add tags to the tag list
|
||||||
@ -232,9 +232,8 @@ export default definePlugin({
|
|||||||
{
|
{
|
||||||
find: ".renderBot=function(){",
|
find: ".renderBot=function(){",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /this.props.user;return null!=(\i)&&.{0,10}\?(.{0,50})\.botTag/,
|
match: /\.BOT;return null!=(\i)&&.{0,10}\?(.{0,50})\.botTag,type:\i/,
|
||||||
replace: "this.props.user;var type=$self.getTag({...this.props,origType:$1.bot?0:null,location:'not-chat'});\
|
replace: ".BOT;var type=$self.getTag({...this.props,origType:$1.bot?0:null,location:'not-chat'});return type!==null?$2.botTag,type"
|
||||||
return type!==null?$2.botTag,type"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// pass channel id down props to be used in profiles
|
// pass channel id down props to be used in profiles
|
||||||
|
@ -54,6 +54,8 @@ interface IVoiceChannelEffectSendEvent {
|
|||||||
const MOYAI = "🗿";
|
const MOYAI = "🗿";
|
||||||
const MOYAI_URL =
|
const MOYAI_URL =
|
||||||
"https://raw.githubusercontent.com/MeguminSama/VencordPlugins/main/plugins/moyai/moyai.mp3";
|
"https://raw.githubusercontent.com/MeguminSama/VencordPlugins/main/plugins/moyai/moyai.mp3";
|
||||||
|
const MOYAI_URL_HD =
|
||||||
|
"https://raw.githubusercontent.com/MeguminSama/VencordPlugins/main/plugins/moyai/moyai_hd.wav";
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
const settings = definePluginSettings({
|
||||||
volume: {
|
volume: {
|
||||||
@ -63,6 +65,14 @@ const settings = definePluginSettings({
|
|||||||
default: 0.5,
|
default: 0.5,
|
||||||
stickToMarkers: false
|
stickToMarkers: false
|
||||||
},
|
},
|
||||||
|
quality: {
|
||||||
|
description: "Quality of the 🗿🗿🗿",
|
||||||
|
type: OptionType.SELECT,
|
||||||
|
options: [
|
||||||
|
{ label: "Normal", value: "Normal", default: true },
|
||||||
|
{ label: "HD", value: "HD" }
|
||||||
|
],
|
||||||
|
},
|
||||||
triggerWhenUnfocused: {
|
triggerWhenUnfocused: {
|
||||||
description: "Trigger the 🗿 even when the window is unfocused",
|
description: "Trigger the 🗿 even when the window is unfocused",
|
||||||
type: OptionType.BOOLEAN,
|
type: OptionType.BOOLEAN,
|
||||||
@ -157,7 +167,11 @@ function getMoyaiCount(message: string) {
|
|||||||
function boom() {
|
function boom() {
|
||||||
if (!settings.store.triggerWhenUnfocused && !document.hasFocus()) return;
|
if (!settings.store.triggerWhenUnfocused && !document.hasFocus()) return;
|
||||||
const audioElement = document.createElement("audio");
|
const audioElement = document.createElement("audio");
|
||||||
audioElement.src = MOYAI_URL;
|
|
||||||
|
audioElement.src = settings.store.quality === "HD"
|
||||||
|
? MOYAI_URL_HD
|
||||||
|
: MOYAI_URL;
|
||||||
|
|
||||||
audioElement.volume = settings.store.volume;
|
audioElement.volume = settings.store.volume;
|
||||||
audioElement.play();
|
audioElement.play();
|
||||||
}
|
}
|
||||||
|
@ -27,10 +27,10 @@ export default definePlugin({
|
|||||||
{
|
{
|
||||||
find: '("ApplicationStreamPreviewUploadManager")',
|
find: '("ApplicationStreamPreviewUploadManager")',
|
||||||
replacement: [
|
replacement: [
|
||||||
"\\i\\.default\\.makeChunkedRequest\\(",
|
String.raw`\i\.\i\.makeChunkedRequest\(`,
|
||||||
"\\i\\.\\i\\.post\\({url:"
|
String.raw`\i\.\i\.post\({url:`
|
||||||
].map(match => ({
|
].map(match => ({
|
||||||
match: new RegExp(`(?=return\\[(\\d),${match}\\i\\.\\i\\.STREAM_PREVIEW.+?}\\)\\];)`),
|
match: new RegExp(String.raw`(?=return\[(\d),${match}\i\.\i\.STREAM_PREVIEW.+?}\)\];)`),
|
||||||
replace: (_, code) => `return[${code},Promise.resolve({body:"",status:204})];`
|
replace: (_, code) => `return[${code},Promise.resolve({body:"",status:204})];`
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
26
src/plugins/normalizeMessageLinks/index.ts
Normal file
26
src/plugins/normalizeMessageLinks/index.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import definePlugin from "@utils/types";
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "NormalizeMessageLinks",
|
||||||
|
description: "Strip canary/ptb from message links",
|
||||||
|
authors: [Devs.bb010g],
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: ".Messages.COPY_MESSAGE_LINK,",
|
||||||
|
replacement: {
|
||||||
|
match: /\.concat\(location\.host\)/,
|
||||||
|
replace: ".concat($self.normalizeHost(location.host))",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
normalizeHost(host: string) {
|
||||||
|
return host.replace(/(^|\b)(canary\.|ptb\.)(discord.com)$/, "$1$3");
|
||||||
|
},
|
||||||
|
});
|
@ -55,7 +55,7 @@ export default definePlugin({
|
|||||||
{
|
{
|
||||||
find: '"MaskedLinkStore"',
|
find: '"MaskedLinkStore"',
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /return ((\i)\.apply\(this,arguments\))(?=\}function \i.{0,200}\.trusted)/,
|
match: /return ((\i)\.apply\(this,arguments\))(?=\}function \i.{0,250}\.trusted)/,
|
||||||
replace: "return $self.handleLink(...arguments).then(handled => handled||$1)"
|
replace: "return $self.handleLink(...arguments).then(handled => handled||$1)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -178,12 +178,12 @@ export default definePlugin({
|
|||||||
start() {
|
start() {
|
||||||
addContextMenuPatch("user-context", this.userContextMenuPatch);
|
addContextMenuPatch("user-context", this.userContextMenuPatch);
|
||||||
addContextMenuPatch("channel-context", this.channelContextMenuPatch);
|
addContextMenuPatch("channel-context", this.channelContextMenuPatch);
|
||||||
addContextMenuPatch("guild-context", this.guildContextMenuPatch);
|
addContextMenuPatch(["guild-context", "guild-header-popout"], this.guildContextMenuPatch);
|
||||||
},
|
},
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
removeContextMenuPatch("user-context", this.userContextMenuPatch);
|
removeContextMenuPatch("user-context", this.userContextMenuPatch);
|
||||||
removeContextMenuPatch("channel-context", this.channelContextMenuPatch);
|
removeContextMenuPatch("channel-context", this.channelContextMenuPatch);
|
||||||
removeContextMenuPatch("guild-context", this.guildContextMenuPatch);
|
removeContextMenuPatch(["guild-context", "guild-header-popout"], this.guildContextMenuPatch);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -18,10 +18,10 @@
|
|||||||
|
|
||||||
import { ApplicationCommandInputType, ApplicationCommandOptionType, Argument, CommandContext, findOption, sendBotMessage } from "@api/Commands";
|
import { ApplicationCommandInputType, ApplicationCommandOptionType, Argument, CommandContext, findOption, sendBotMessage } from "@api/Commands";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { getGifEncoder } from "@utils/dependencies";
|
|
||||||
import { makeLazy } from "@utils/lazy";
|
import { makeLazy } from "@utils/lazy";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { findByCodeLazy, findByPropsLazy } from "@webpack";
|
import { findByCodeLazy, findByPropsLazy } from "@webpack";
|
||||||
|
import { applyPalette, GIFEncoder, quantize } from "gifenc";
|
||||||
|
|
||||||
const DRAFT_TYPE = 0;
|
const DRAFT_TYPE = 0;
|
||||||
const DEFAULT_DELAY = 20;
|
const DEFAULT_DELAY = 20;
|
||||||
@ -124,7 +124,6 @@ export default definePlugin({
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
execute: async (opts, cmdCtx) => {
|
execute: async (opts, cmdCtx) => {
|
||||||
const { GIFEncoder, quantize, applyPalette } = await getGifEncoder();
|
|
||||||
const frames = await getFrames();
|
const frames = await getFrames();
|
||||||
|
|
||||||
const noServerPfp = findOption(opts, "no-server-pfp", false);
|
const noServerPfp = findOption(opts, "no-server-pfp", false);
|
||||||
@ -143,7 +142,7 @@ export default definePlugin({
|
|||||||
const delay = findOption(opts, "delay", DEFAULT_DELAY);
|
const delay = findOption(opts, "delay", DEFAULT_DELAY);
|
||||||
const resolution = findOption(opts, "resolution", DEFAULT_RESOLUTION);
|
const resolution = findOption(opts, "resolution", DEFAULT_RESOLUTION);
|
||||||
|
|
||||||
const gif = new GIFEncoder();
|
const gif = GIFEncoder();
|
||||||
|
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
canvas.width = canvas.height = resolution;
|
canvas.width = canvas.height = resolution;
|
||||||
|
83
src/plugins/pictureInPicture.tsx
Normal file
83
src/plugins/pictureInPicture.tsx
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { definePluginSettings } from "@api/Settings";
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
|
import { React, Tooltip } from "@webpack/common";
|
||||||
|
|
||||||
|
const settings = definePluginSettings({
|
||||||
|
loop: {
|
||||||
|
description: "Whether to make the PiP video loop or not",
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
default: true,
|
||||||
|
restartNeeded: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "PictureInPicture",
|
||||||
|
description: "Adds picture in picture to videos (next to the Download button)",
|
||||||
|
authors: [Devs.Lumap],
|
||||||
|
settings,
|
||||||
|
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: ".onRemoveAttachment,",
|
||||||
|
replacement: {
|
||||||
|
match: /\.nonMediaAttachment,!(\i).{0,7}children:\[(\i),/,
|
||||||
|
replace: "$&$1&&$2&&$self.renderPiPButton(),"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
renderPiPButton: ErrorBoundary.wrap(() => {
|
||||||
|
return (
|
||||||
|
<Tooltip text="Toggle Picture in Picture">
|
||||||
|
{tooltipProps => (
|
||||||
|
<div
|
||||||
|
{...tooltipProps}
|
||||||
|
role="button"
|
||||||
|
style={{
|
||||||
|
cursor: "pointer",
|
||||||
|
paddingTop: "4px",
|
||||||
|
paddingLeft: "4px",
|
||||||
|
paddingRight: "4px",
|
||||||
|
}}
|
||||||
|
onClick={e => {
|
||||||
|
const video = e.currentTarget.parentNode!.parentNode!.querySelector("video")!;
|
||||||
|
const videoClone = document.body.appendChild(video.cloneNode(true)) as HTMLVideoElement;
|
||||||
|
|
||||||
|
videoClone.loop = settings.store.loop;
|
||||||
|
videoClone.style.display = "none";
|
||||||
|
videoClone.onleavepictureinpicture = () => videoClone.remove();
|
||||||
|
|
||||||
|
function launchPiP() {
|
||||||
|
videoClone.currentTime = video.currentTime;
|
||||||
|
videoClone.requestPictureInPicture();
|
||||||
|
video.pause();
|
||||||
|
videoClone.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (videoClone.readyState === 4 /* HAVE_ENOUGH_DATA */)
|
||||||
|
launchPiP();
|
||||||
|
else
|
||||||
|
videoClone.onloadedmetadata = launchPiP;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg width="24px" height="24px" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="var(--interactive-normal)"
|
||||||
|
d="M21 3a1 1 0 0 1 1 1v7h-2V5H4v14h6v2H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h18zm0 10a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1h-8a1 1 0 0 1-1-1v-6a1 1 0 0 1 1-1h8zm-1 2h-6v4h6v-4z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}, { noop: true })
|
||||||
|
});
|
@ -122,6 +122,14 @@ export default definePlugin({
|
|||||||
// ....concat(pins).concat(toArray(channelIds).filter(c => !isPinned(c)))
|
// ....concat(pins).concat(toArray(channelIds).filter(c => !isPinned(c)))
|
||||||
replace: ".concat($self.getSnapshot()).concat($2.filter(c=>!$self.isPinned(c)))"
|
replace: ".concat($self.getSnapshot()).concat($2.filter(c=>!$self.isPinned(c)))"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
// fix alt+shift+up/down
|
||||||
|
{
|
||||||
|
find: '"alt+shift+down"',
|
||||||
|
replacement: {
|
||||||
|
match: /(?<=return \i===\i\.ME\?)\i\.\i\.getPrivateChannelIds\(\)/,
|
||||||
|
replace: "$self.getSnapshot().concat($&.filter(c=>!$self.isPinned(c)))"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
5
src/plugins/previewMessage/README.md
Normal file
5
src/plugins/previewMessage/README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# PreviewMessage
|
||||||
|
|
||||||
|
Lets you preview your message before sending it.
|
||||||
|
|
||||||
|
![Example](https://i.imgur.com/etqbkzu.png)
|
141
src/plugins/previewMessage/index.tsx
Normal file
141
src/plugins/previewMessage/index.tsx
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
/*
|
||||||
|
* 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 { generateId, sendBotMessage } from "@api/Commands";
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import definePlugin from "@utils/types";
|
||||||
|
import { findByPropsLazy } from "@webpack";
|
||||||
|
import { Button, ButtonLooks, ButtonWrapperClasses, DraftStore, DraftType, SelectedChannelStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common";
|
||||||
|
import { MessageAttachment } from "discord-types/general";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
type: {
|
||||||
|
analyticsName: string;
|
||||||
|
isEmpty: boolean;
|
||||||
|
attachments: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const UploadStore = findByPropsLazy("getUploads");
|
||||||
|
|
||||||
|
const getDraft = (channelId: string) => DraftStore.getDraft(channelId, DraftType.ChannelMessage);
|
||||||
|
|
||||||
|
|
||||||
|
const getImageBox = (url: string): Promise<{ width: number, height: number; } | null> =>
|
||||||
|
new Promise(res => {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () =>
|
||||||
|
res({ width: img.width, height: img.height });
|
||||||
|
|
||||||
|
img.onerror = () =>
|
||||||
|
res(null);
|
||||||
|
|
||||||
|
img.src = url;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const getAttachments = async (channelId: string) =>
|
||||||
|
await Promise.all(
|
||||||
|
UploadStore.getUploads(channelId, DraftType.ChannelMessage)
|
||||||
|
.map(async (upload: any) => {
|
||||||
|
const { isImage, filename, spoiler, item: { file } } = upload;
|
||||||
|
const url = URL.createObjectURL(file);
|
||||||
|
const attachment: MessageAttachment = {
|
||||||
|
id: generateId(),
|
||||||
|
filename: spoiler ? "SPOILER_" + filename : filename,
|
||||||
|
// weird eh? if i give it the normal content type the preview doenst work
|
||||||
|
content_type: undefined,
|
||||||
|
size: await upload.getSize(),
|
||||||
|
spoiler,
|
||||||
|
// discord adds query params to the url, so we need to add a hash to prevent that
|
||||||
|
url: url + "#",
|
||||||
|
proxy_url: url + "#",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isImage) {
|
||||||
|
const box = await getImageBox(url);
|
||||||
|
if (!box) return attachment;
|
||||||
|
|
||||||
|
attachment.width = box.width;
|
||||||
|
attachment.height = box.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
return attachment;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
export function PreviewButton(chatBoxProps: Props) {
|
||||||
|
const { isEmpty, attachments } = chatBoxProps.type;
|
||||||
|
|
||||||
|
const channelId = SelectedChannelStore.getChannelId();
|
||||||
|
const draft = useStateFromStores([DraftStore], () => getDraft(channelId));
|
||||||
|
|
||||||
|
if (chatBoxProps.type.analyticsName !== "normal") return null;
|
||||||
|
|
||||||
|
const hasAttachments = attachments && UploadStore.getUploads(channelId, DraftType.ChannelMessage).length > 0;
|
||||||
|
const hasContent = !isEmpty && draft?.length > 0;
|
||||||
|
|
||||||
|
if (!hasContent && !hasAttachments) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip text="Preview Message">
|
||||||
|
{tooltipProps => (
|
||||||
|
<Button
|
||||||
|
{...tooltipProps}
|
||||||
|
onClick={async () =>
|
||||||
|
sendBotMessage(
|
||||||
|
channelId,
|
||||||
|
{
|
||||||
|
content: getDraft(channelId),
|
||||||
|
author: UserStore.getCurrentUser(),
|
||||||
|
attachments: hasAttachments ? await getAttachments(channelId) : undefined,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
size=""
|
||||||
|
look={ButtonLooks.BLANK}
|
||||||
|
innerClassName={ButtonWrapperClasses.button}
|
||||||
|
style={{ padding: "0 2px", height: "100%" }}
|
||||||
|
>
|
||||||
|
<div className={ButtonWrapperClasses.buttonWrapper}>
|
||||||
|
<img width={24} height={24} src="https://discord.com/assets/4c5a77a89716352686f590a6f014770c.svg" />
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "PreviewMessage",
|
||||||
|
description: "Lets you preview your message before sending it.",
|
||||||
|
authors: [Devs.Aria],
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: ".activeCommandOption",
|
||||||
|
replacement: {
|
||||||
|
match: /(.)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/,
|
||||||
|
replace: "$&;try{$2||$1.unshift($self.previewIcon(arguments[0]))}catch{}",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
previewIcon: ErrorBoundary.wrap(PreviewButton, { noop: true }),
|
||||||
|
});
|
@ -68,7 +68,8 @@ export default definePlugin({
|
|||||||
find: ".USER_PROFILE_ACTIVITY",
|
find: ".USER_PROFILE_ACTIVITY",
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /getGlobalName\(\i\);(?<=displayProfile.{0,200})/,
|
/* FIXME: old name is getGlobalName, new name is getName. Remove optional Global once stable has also migrated */
|
||||||
|
match: /\.get(?:Global)?Name\(\i\);(?<=displayProfile.{0,200})/,
|
||||||
replace: "$&const [vcPronounce,vcPronounSource]=$self.useProfilePronouns(arguments[0].user.id,true);if(arguments[0].displayProfile&&vcPronounce)arguments[0].displayProfile.pronouns=vcPronounce;"
|
replace: "$&const [vcPronounce,vcPronounSource]=$self.useProfilePronouns(arguments[0].user.id,true);if(arguments[0].displayProfile&&vcPronounce)arguments[0].displayProfile.pronouns=vcPronounce;"
|
||||||
},
|
},
|
||||||
PRONOUN_TOOLTIP_PATCH
|
PRONOUN_TOOLTIP_PATCH
|
||||||
|
@ -21,14 +21,11 @@ import { VENCORD_USER_AGENT } from "@utils/constants";
|
|||||||
import { debounce } from "@utils/debounce";
|
import { debounce } from "@utils/debounce";
|
||||||
import { getCurrentChannel } from "@utils/discord";
|
import { getCurrentChannel } from "@utils/discord";
|
||||||
import { useAwaiter } from "@utils/react";
|
import { useAwaiter } from "@utils/react";
|
||||||
import { findStoreLazy } from "@webpack";
|
import { UserProfileStore, UserStore } from "@webpack/common";
|
||||||
import { UserStore } from "@webpack/common";
|
|
||||||
|
|
||||||
import { settings } from "./settings";
|
import { settings } from "./settings";
|
||||||
import { PronounCode, PronounMapping, PronounsResponse } from "./types";
|
import { PronounCode, PronounMapping, PronounsResponse } from "./types";
|
||||||
|
|
||||||
const UserProfileStore = findStoreLazy("UserProfileStore");
|
|
||||||
|
|
||||||
type PronounsWithSource = [string | null, string];
|
type PronounsWithSource = [string | null, string];
|
||||||
const EmptyPronouns: PronounsWithSource = [null, ""];
|
const EmptyPronouns: PronounsWithSource = [null, ""];
|
||||||
|
|
||||||
|
@ -28,8 +28,8 @@ export default function ReviewBadge(badge: Badge) {
|
|||||||
{({ onMouseEnter, onMouseLeave }) => (
|
{({ onMouseEnter, onMouseLeave }) => (
|
||||||
<img
|
<img
|
||||||
className={cl("badge")}
|
className={cl("badge")}
|
||||||
width="24px"
|
width="22px"
|
||||||
height="24px"
|
height="22px"
|
||||||
onMouseEnter={onMouseEnter}
|
onMouseEnter={onMouseEnter}
|
||||||
onMouseLeave={onMouseLeave}
|
onMouseLeave={onMouseLeave}
|
||||||
src={badge.icon}
|
src={badge.icon}
|
||||||
|
@ -20,7 +20,7 @@ import { openUserProfile } from "@utils/discord";
|
|||||||
import { classes } from "@utils/misc";
|
import { classes } from "@utils/misc";
|
||||||
import { LazyComponent } from "@utils/react";
|
import { LazyComponent } from "@utils/react";
|
||||||
import { filters, findBulk } from "@webpack";
|
import { filters, findBulk } from "@webpack";
|
||||||
import { Alerts, moment, Timestamp, UserStore } from "@webpack/common";
|
import { Alerts, moment, Parser, Timestamp, UserStore } from "@webpack/common";
|
||||||
|
|
||||||
import { Review, ReviewType } from "../entities";
|
import { Review, ReviewType } from "../entities";
|
||||||
import { deleteReview, reportReview } from "../reviewDbApi";
|
import { deleteReview, reportReview } from "../reviewDbApi";
|
||||||
@ -30,12 +30,12 @@ import { DeleteButton, ReportButton } from "./MessageButton";
|
|||||||
import ReviewBadge from "./ReviewBadge";
|
import ReviewBadge from "./ReviewBadge";
|
||||||
|
|
||||||
export default LazyComponent(() => {
|
export default LazyComponent(() => {
|
||||||
// this is terrible, blame mantika
|
// this is terrible, blame ven
|
||||||
const p = filters.byProps;
|
const p = filters.byProps;
|
||||||
const [
|
const [
|
||||||
{ cozyMessage, buttons, message, groupStart },
|
{ cozyMessage, buttons, message, buttonsInner, groupStart },
|
||||||
{ container, isHeader },
|
{ container, isHeader },
|
||||||
{ avatar, clickable, username, messageContent, wrapper, cozy },
|
{ avatar, clickable, username, wrapper, cozy },
|
||||||
buttonClasses,
|
buttonClasses,
|
||||||
botTag
|
botTag
|
||||||
] = findBulk(
|
] = findBulk(
|
||||||
@ -124,17 +124,15 @@ export default LazyComponent(() => {
|
|||||||
</Timestamp>)
|
</Timestamp>)
|
||||||
}
|
}
|
||||||
|
|
||||||
<p
|
<div className={cl("review-comment")}>
|
||||||
className={classes(messageContent)}
|
{Parser.parseGuildEventDescription(review.comment)}
|
||||||
style={{ fontSize: 15, marginTop: 4, color: "var(--text-normal)" }}
|
</div>
|
||||||
>
|
|
||||||
{review.comment}
|
|
||||||
</p>
|
|
||||||
{review.id !== 0 && (
|
{review.id !== 0 && (
|
||||||
<div className={classes(container, isHeader, buttons)} style={{
|
<div className={classes(container, isHeader, buttons)} style={{
|
||||||
padding: "0px",
|
padding: "0px",
|
||||||
}}>
|
}}>
|
||||||
<div className={buttonClasses.wrapper} >
|
<div className={classes(buttonClasses.wrapper, buttonsInner)} >
|
||||||
<ReportButton onClick={reportRev} />
|
<ReportButton onClick={reportRev} />
|
||||||
|
|
||||||
{canDeleteReview(review, UserStore.getCurrentUser().id) && (
|
{canDeleteReview(review, UserStore.getCurrentUser().id) && (
|
||||||
|
@ -16,11 +16,9 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { classes } from "@utils/misc";
|
import { LazyComponent, useAwaiter, useForceUpdater } from "@utils/react";
|
||||||
import { useAwaiter, useForceUpdater } from "@utils/react";
|
import { find, findByPropsLazy } from "@webpack";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { Forms, React, RelationshipStore, useRef, UserStore } from "@webpack/common";
|
||||||
import { Forms, React, RelationshipStore, UserStore } from "@webpack/common";
|
|
||||||
import type { KeyboardEvent } from "react";
|
|
||||||
|
|
||||||
import { Review } from "../entities";
|
import { Review } from "../entities";
|
||||||
import { addReview, getReviews, Response, REVIEWS_PER_PAGE } from "../reviewDbApi";
|
import { addReview, getReviews, Response, REVIEWS_PER_PAGE } from "../reviewDbApi";
|
||||||
@ -28,7 +26,12 @@ import { settings } from "../settings";
|
|||||||
import { authorize, cl, showToast } from "../utils";
|
import { authorize, cl, showToast } from "../utils";
|
||||||
import ReviewComponent from "./ReviewComponent";
|
import ReviewComponent from "./ReviewComponent";
|
||||||
|
|
||||||
const Classes = findByPropsLazy("inputDefault", "editable");
|
|
||||||
|
const Editor = findByPropsLazy("start", "end", "addMark");
|
||||||
|
const Transform = findByPropsLazy("unwrapNodes");
|
||||||
|
const InputTypes = findByPropsLazy("VOICE_CHANNEL_STATUS", "SIDEBAR");
|
||||||
|
|
||||||
|
const InputComponent = LazyComponent(() => find(m => m?.type?.render?.toString().includes("CHANNEL_TEXT_AREA).AnalyticsLocationProvider")));
|
||||||
|
|
||||||
interface UserProps {
|
interface UserProps {
|
||||||
discordId: string;
|
discordId: string;
|
||||||
@ -113,34 +116,38 @@ function ReviewList({ refetch, reviews, hideOwnReview }: { refetch(): void; revi
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function ReviewsInputComponent({ discordId, isAuthor, refetch, name }: { discordId: string, name: string; isAuthor: boolean; refetch(): void; }) {
|
export function ReviewsInputComponent({ discordId, isAuthor, refetch, name }: { discordId: string, name: string; isAuthor: boolean; refetch(): void; }) {
|
||||||
const { token } = settings.store;
|
const { token } = settings.store;
|
||||||
|
const editorRef = useRef<any>(null);
|
||||||
|
const inputType = InputTypes.FORM;
|
||||||
|
inputType.disableAutoFocus = true;
|
||||||
|
|
||||||
function onKeyPress({ key, target }: KeyboardEvent<HTMLTextAreaElement>) {
|
const channel = {
|
||||||
if (key === "Enter") {
|
flags_: 256,
|
||||||
addReview({
|
guild_id_: null,
|
||||||
userid: discordId,
|
id: "0",
|
||||||
comment: (target as HTMLInputElement).value,
|
getGuildId: () => null,
|
||||||
star: -1
|
isPrivate: () => true,
|
||||||
}).then(res => {
|
isActiveThread: () => false,
|
||||||
if (res?.success) {
|
isArchivedLockedThread: () => false,
|
||||||
(target as HTMLInputElement).value = ""; // clear the input
|
isDM: () => true,
|
||||||
refetch();
|
roles: { "0": { permissions: 0n } },
|
||||||
} else if (res?.message) {
|
getRecipientId: () => "0",
|
||||||
showToast(res.message);
|
hasFlag: () => false,
|
||||||
}
|
};
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<textarea
|
<>
|
||||||
className={classes(Classes.inputDefault, "enter-comment", cl("input"))}
|
<div onClick={() => {
|
||||||
onKeyDownCapture={e => {
|
if (!token) {
|
||||||
if (e.key === "Enter") {
|
showToast("Opening authorization window...");
|
||||||
e.preventDefault(); // prevent newlines
|
authorize();
|
||||||
}
|
}
|
||||||
}}
|
}}>
|
||||||
|
<InputComponent
|
||||||
|
className={cl("input")}
|
||||||
|
channel={channel}
|
||||||
placeholder={
|
placeholder={
|
||||||
!token
|
!token
|
||||||
? "You need to authorize to review users!"
|
? "You need to authorize to review users!"
|
||||||
@ -148,13 +155,43 @@ export function ReviewsInputComponent({ discordId, isAuthor, refetch, name }: {
|
|||||||
? `Update review for @${name}`
|
? `Update review for @${name}`
|
||||||
: `Review @${name}`
|
: `Review @${name}`
|
||||||
}
|
}
|
||||||
onKeyDown={onKeyPress}
|
type={inputType}
|
||||||
onClick={() => {
|
disableThemedBackground={true}
|
||||||
if (!token) {
|
setEditorRef={ref => editorRef.current = ref}
|
||||||
showToast("Opening authorization window...");
|
textValue=""
|
||||||
authorize();
|
onSubmit={
|
||||||
|
async res => {
|
||||||
|
const response = await addReview({
|
||||||
|
userid: discordId,
|
||||||
|
comment: res.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response?.success) {
|
||||||
|
refetch();
|
||||||
|
|
||||||
|
const slateEditor = editorRef.current.ref.current.getSlateEditor();
|
||||||
|
|
||||||
|
// clear editor
|
||||||
|
Transform.delete(slateEditor, {
|
||||||
|
at: {
|
||||||
|
anchor: Editor.start(slateEditor, []),
|
||||||
|
focus: Editor.end(slateEditor, []),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (response?.message) {
|
||||||
|
showToast(response.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// even tho we need to return this, it doesnt do anything
|
||||||
|
return {
|
||||||
|
shouldClear: false,
|
||||||
|
shouldRefocus: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,13 @@ export const enum ReviewType {
|
|||||||
System = 3
|
System = 3
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const enum NotificationType {
|
||||||
|
Info = 0,
|
||||||
|
Ban = 1,
|
||||||
|
Unban = 2,
|
||||||
|
Warning = 3
|
||||||
|
}
|
||||||
|
|
||||||
export interface Badge {
|
export interface Badge {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
@ -45,6 +52,13 @@ export interface BanInfo {
|
|||||||
banEndDate: number;
|
banEndDate: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Notification {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
type: NotificationType;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ReviewDBUser {
|
export interface ReviewDBUser {
|
||||||
ID: number;
|
ID: number;
|
||||||
discordID: string;
|
discordID: string;
|
||||||
@ -54,6 +68,7 @@ export interface ReviewDBUser {
|
|||||||
warningCount: number;
|
warningCount: number;
|
||||||
badges: any[];
|
badges: any[];
|
||||||
banInfo: BanInfo | null;
|
banInfo: BanInfo | null;
|
||||||
|
notification: Notification | null;
|
||||||
lastReviewID: number;
|
lastReviewID: number;
|
||||||
type: UserType;
|
type: UserType;
|
||||||
}
|
}
|
||||||
|
@ -24,13 +24,13 @@ import ExpandableHeader from "@components/ExpandableHeader";
|
|||||||
import { OpenExternalIcon } from "@components/Icons";
|
import { OpenExternalIcon } from "@components/Icons";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { Alerts, Menu, useState } from "@webpack/common";
|
import { Alerts, Menu, Parser, useState } from "@webpack/common";
|
||||||
import { Guild, User } from "discord-types/general";
|
import { Guild, User } from "discord-types/general";
|
||||||
|
|
||||||
import { openReviewsModal } from "./components/ReviewModal";
|
import { openReviewsModal } from "./components/ReviewModal";
|
||||||
import ReviewsView from "./components/ReviewsView";
|
import ReviewsView from "./components/ReviewsView";
|
||||||
import { UserType } from "./entities";
|
import { NotificationType } from "./entities";
|
||||||
import { getCurrentUserInfo } from "./reviewDbApi";
|
import { getCurrentUserInfo, readNotification } from "./reviewDbApi";
|
||||||
import { settings } from "./settings";
|
import { settings } from "./settings";
|
||||||
import { showToast } from "./utils";
|
import { showToast } from "./utils";
|
||||||
|
|
||||||
@ -78,26 +78,8 @@ export default definePlugin({
|
|||||||
|
|
||||||
addContextMenuPatch("guild-header-popout", guildPopoutPatch);
|
addContextMenuPatch("guild-header-popout", guildPopoutPatch);
|
||||||
|
|
||||||
if (user.banInfo) {
|
if (user.notification) {
|
||||||
const endDate = new Date(user.banInfo.banEndDate);
|
const props = user.notification.type === NotificationType.Ban ? {
|
||||||
if (endDate.getTime() > Date.now() && (s.user?.banInfo?.banEndDate ?? 0) < endDate.getTime()) {
|
|
||||||
Alerts.show({
|
|
||||||
title: "You have been banned from ReviewDB",
|
|
||||||
body: (
|
|
||||||
<>
|
|
||||||
<p>
|
|
||||||
You are banned from ReviewDB {
|
|
||||||
user.type === UserType.Banned
|
|
||||||
? "permanently"
|
|
||||||
: "until " + endDate.toLocaleString()
|
|
||||||
}
|
|
||||||
</p>
|
|
||||||
{user.banInfo.reviewContent && (
|
|
||||||
<p>Offending Review: {user.banInfo.reviewContent}</p>
|
|
||||||
)}
|
|
||||||
<p>Continued offenses will result in a permanent ban.</p>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
cancelText: "Appeal",
|
cancelText: "Appeal",
|
||||||
confirmText: "Ok",
|
confirmText: "Ok",
|
||||||
onCancel: () =>
|
onCancel: () =>
|
||||||
@ -108,10 +90,21 @@ export default definePlugin({
|
|||||||
page: "dashboard/appeal"
|
page: "dashboard/appeal"
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
});
|
} : {};
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
Alerts.show({
|
||||||
|
title: user.notification.title,
|
||||||
|
body: (
|
||||||
|
Parser.parse(
|
||||||
|
user.notification.content,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
),
|
||||||
|
...props
|
||||||
|
});
|
||||||
|
|
||||||
|
readNotification(user.notification.id);
|
||||||
|
}
|
||||||
s.user = user;
|
s.user = user;
|
||||||
}, 4000);
|
}, 4000);
|
||||||
},
|
},
|
||||||
|
@ -140,3 +140,12 @@ export function getCurrentUserInfo(token: string): Promise<ReviewDBUser> {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
}).then(r => r.json());
|
}).then(r => r.json());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function readNotification(id: number) {
|
||||||
|
return fetch(API_URL + `/api/reviewdb/notifications?id=${id}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: {
|
||||||
|
"Authorization": settings.store.token || "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@ -14,7 +14,16 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 1px solid var(--profile-message-input-border-color);
|
border: 1px solid var(--profile-message-input-border-color);
|
||||||
font-size: 14px;
|
}
|
||||||
|
|
||||||
|
.vc-rdb-modal-footer > div {
|
||||||
|
width: 100%;
|
||||||
|
margin: 6px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* When input becomes disabled(while sending review), input adds unneccesary padding to left, this prevents it */
|
||||||
|
.vc-rdb-input > div > div {
|
||||||
|
padding-left: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-rdb-placeholder {
|
.vc-rdb-placeholder {
|
||||||
@ -24,13 +33,12 @@
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-rdb-modal-footer {
|
.vc-rdb-input * {
|
||||||
padding: 0;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-rdb-modal-footer > div {
|
.vc-rdb-modal-footer {
|
||||||
width: 100%;
|
padding: 0;
|
||||||
margin: 6px 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-rdb-modal-footer .vc-rdb-input {
|
.vc-rdb-modal-footer .vc-rdb-input {
|
||||||
@ -49,3 +57,20 @@
|
|||||||
.vc-rdb-modal-reviews {
|
.vc-rdb-modal-reviews {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vc-rdb-review {
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-rdb-review-comment img {
|
||||||
|
vertical-align: text-top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-rdb-review-comment {
|
||||||
|
overflow-y: hidden;
|
||||||
|
margin-top: 1px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: var(--text-normal);
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
21
src/plugins/rnnoise.web/icons.tsx
Normal file
21
src/plugins/rnnoise.web/icons.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const SupressionIcon = ({ enabled }: { enabled: boolean; }) => enabled
|
||||||
|
? <svg aria-hidden="true" role="img" width="20" height="20" viewBox="0 0 24 24"><path d="M10.889 4C10.889 3.44772 11.3367 3 11.889 3H12.1112C12.6635 3 13.1112 3.44772 13.1112 4V20C13.1112 20.5523 12.6635 21 12.1112 21H11.889C11.3367 21 10.889 20.5523 10.889 20V4Z" fill="currentColor"></path><path d="M6.44439 6.25C6.44439 5.69772 6.89211 5.25 7.44439 5.25H7.66661C8.2189 5.25 8.66661 5.69772 8.66661 6.25V17.75C8.66661 18.3023 8.2189 18.75 7.66661 18.75H7.44439C6.89211 18.75 6.44439 18.3023 6.44439 17.75V6.25Z" fill="currentColor"></path><path d="M3.22222 15.375C3.77451 15.375 4.22222 14.9273 4.22222 14.375L4.22222 9.625C4.22222 9.07272 3.77451 8.625 3.22222 8.625H3C2.44772 8.625 2 9.07272 2 9.625V14.375C2 14.9273 2.44772 15.375 3 15.375H3.22222Z" fill="currentColor"></path><path d="M22.0001 13.25C22.0001 13.8023 21.5523 14.25 21.0001 14.25H20.7778C20.2255 14.25 19.7778 13.8023 19.7778 13.25V10.75C19.7778 10.1977 20.2255 9.75 20.7778 9.75H21.0001C21.5523 9.75 22.0001 10.1977 22.0001 10.75V13.25Z" fill="currentColor"></path><path d="M16.3333 7.5C15.781 7.5 15.3333 7.94772 15.3333 8.5V15.5C15.3333 16.0523 15.781 16.5 16.3333 16.5H16.5555C17.1078 16.5 17.5555 16.0523 17.5555 15.5V8.5C17.5555 7.94772 17.1078 7.5 16.5555 7.5H16.3333Z" fill="currentColor"></path></svg>
|
||||||
|
: <svg aria-hidden="true" role="img" width="20" height="20" viewBox="0 0 48 48"><path d="M30.6666 24.9644L35.1111 20.5199V31C35.1111 32.1046 34.2156 33 33.1111 33H32.6666C31.562 33 30.6666 32.1046 30.6666 31V24.9644Z" fill="currentColor"></path><path d="M26.2224 14.1463V8C26.2224 6.89543 25.327 6 24.2224 6H23.7779C22.6734 6 21.7779 6.89543 21.7779 8V18.5907L26.2224 14.1463Z" fill="currentColor"></path><path d="M21.7779 33.8543L21.9254 33.7056L26.2224 29.4086V40C26.2224 41.1046 25.327 42 24.2224 42H23.7779C22.6734 42 21.7779 41.1046 21.7779 40V33.8543Z" fill="currentColor"></path><path d="M17.3332 23.0354L12.8888 27.4799V12.5C12.8888 11.3954 13.7842 10.5 14.8888 10.5H15.3332C16.4378 10.5 17.3332 11.3954 17.3332 12.5V23.0354Z" fill="currentColor"></path><path d="M8.44445 28.75C8.44445 29.8546 7.54902 30.75 6.44445 30.75H6C4.89543 30.75 4 29.8546 4 28.75V19.25C4 18.1454 4.89543 17.25 6 17.25H6.44445C7.54902 17.25 8.44445 18.1454 8.44445 19.25L8.44445 28.75Z" fill="currentColor"></path><path d="M44.0001 26.5C44.0001 27.6046 43.1047 28.5 42.0001 28.5H41.5557C40.4511 28.5 39.5557 27.6046 39.5557 26.5V21.5C39.5557 20.3954 40.4511 19.5 41.5557 19.5H42.0001C43.1047 19.5 44.0001 20.3954 44.0001 21.5V26.5Z" fill="currentColor"></path><path d="M42 8.54L39.46 6L6 39.46L8.54 42L16.92 33.64L19.38 31.16L22.7 27.84L29.98 20.56L42 8.54Z" fill="currentColor"></path></svg>;
|
250
src/plugins/rnnoise.web/index.tsx
Normal file
250
src/plugins/rnnoise.web/index.tsx
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
/*
|
||||||
|
* 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 "./styles.css";
|
||||||
|
|
||||||
|
import { definePluginSettings } from "@api/Settings";
|
||||||
|
import { classNameFactory } from "@api/Styles";
|
||||||
|
import { Switch } from "@components/Switch";
|
||||||
|
import { loadRnnoise, RnnoiseWorkletNode } from "@sapphi-red/web-noise-suppressor";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import { rnnoiseWasmSrc, rnnoiseWorkletSrc } from "@utils/dependencies";
|
||||||
|
import { makeLazy } from "@utils/lazy";
|
||||||
|
import { Logger } from "@utils/Logger";
|
||||||
|
import { LazyComponent } from "@utils/react";
|
||||||
|
import definePlugin from "@utils/types";
|
||||||
|
import { findByCode } from "@webpack";
|
||||||
|
import { FluxDispatcher, Popout, React } from "@webpack/common";
|
||||||
|
import { MouseEvent, ReactNode } from "react";
|
||||||
|
|
||||||
|
import { SupressionIcon } from "./icons";
|
||||||
|
|
||||||
|
const RNNOISE_OPTION = "RNNOISE";
|
||||||
|
|
||||||
|
interface PanelButtonProps {
|
||||||
|
tooltipText: string;
|
||||||
|
icon: () => ReactNode;
|
||||||
|
onClick: (event: MouseEvent<HTMLElement>) => void;
|
||||||
|
tooltipClassName?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
shouldShow?: boolean;
|
||||||
|
}
|
||||||
|
const PanelButton = LazyComponent<PanelButtonProps>(() => findByCode("Masks.PANEL_BUTTON"));
|
||||||
|
const enum SpinnerType {
|
||||||
|
SpinningCircle = "spinningCircle",
|
||||||
|
ChasingDots = "chasingDots",
|
||||||
|
LowMotion = "lowMotion",
|
||||||
|
PulsingEllipsis = "pulsingEllipsis",
|
||||||
|
WanderingCubes = "wanderingCubes",
|
||||||
|
}
|
||||||
|
export interface SpinnerProps {
|
||||||
|
type: SpinnerType;
|
||||||
|
animated?: boolean;
|
||||||
|
className?: string;
|
||||||
|
itemClassName?: string;
|
||||||
|
}
|
||||||
|
const Spinner = LazyComponent<SpinnerProps>(() => findByCode(".spinningCircleInner"));
|
||||||
|
|
||||||
|
function createExternalStore<S>(init: () => S) {
|
||||||
|
const subscribers = new Set<() => void>();
|
||||||
|
let state = init();
|
||||||
|
|
||||||
|
return {
|
||||||
|
get: () => state,
|
||||||
|
set: (newStateGetter: (oldState: S) => S) => {
|
||||||
|
state = newStateGetter(state);
|
||||||
|
for (const cb of subscribers) cb();
|
||||||
|
},
|
||||||
|
use: () => {
|
||||||
|
return React.useSyncExternalStore<S>(onStoreChange => {
|
||||||
|
subscribers.add(onStoreChange);
|
||||||
|
return () => subscribers.delete(onStoreChange);
|
||||||
|
}, () => state);
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cl = classNameFactory("vc-rnnoise-");
|
||||||
|
|
||||||
|
const loadedStore = createExternalStore(() => ({
|
||||||
|
isLoaded: false,
|
||||||
|
isLoading: false,
|
||||||
|
isError: false,
|
||||||
|
}));
|
||||||
|
const getRnnoiseWasm = makeLazy(() => {
|
||||||
|
loadedStore.set(s => ({ ...s, isLoading: true }));
|
||||||
|
return loadRnnoise({
|
||||||
|
url: rnnoiseWasmSrc(),
|
||||||
|
simdUrl: rnnoiseWasmSrc(true),
|
||||||
|
}).then(buffer => {
|
||||||
|
// Check WASM magic number cus fetch doesnt throw on 4XX or 5XX
|
||||||
|
if (new DataView(buffer.slice(0, 4)).getUint32(0) !== 0x0061736D) throw buffer;
|
||||||
|
|
||||||
|
loadedStore.set(s => ({ ...s, isLoaded: true }));
|
||||||
|
return buffer;
|
||||||
|
}).catch(error => {
|
||||||
|
if (error instanceof ArrayBuffer) error = new TextDecoder().decode(error);
|
||||||
|
logger.error("Failed to load RNNoise WASM:", error);
|
||||||
|
loadedStore.set(s => ({ ...s, isError: true }));
|
||||||
|
return null;
|
||||||
|
}).finally(() => {
|
||||||
|
loadedStore.set(s => ({ ...s, isLoading: false }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const logger = new Logger("RNNoise");
|
||||||
|
const settings = definePluginSettings({}).withPrivateSettings<{ isEnabled: boolean; }>();
|
||||||
|
const setEnabled = (enabled: boolean) => {
|
||||||
|
settings.store.isEnabled = enabled;
|
||||||
|
FluxDispatcher.dispatch({ type: "AUDIO_SET_NOISE_SUPPRESSION", enabled });
|
||||||
|
};
|
||||||
|
|
||||||
|
function NoiseSupressionPopout() {
|
||||||
|
const { isEnabled } = settings.use();
|
||||||
|
const { isLoading, isError } = loadedStore.use();
|
||||||
|
const isWorking = isEnabled && !isError;
|
||||||
|
|
||||||
|
return <div className={cl("popout")}>
|
||||||
|
<div className={cl("popout-heading")}>
|
||||||
|
<span>Noise Supression</span>
|
||||||
|
<div style={{ flex: 1 }} />
|
||||||
|
{isLoading && <Spinner type={SpinnerType.PulsingEllipsis} />}
|
||||||
|
<Switch checked={isWorking} onChange={setEnabled} disabled={isError} />
|
||||||
|
</div>
|
||||||
|
<div className={cl("popout-desc")}>
|
||||||
|
Enable AI noise suppression! Make some noise—like becoming an air conditioner, or a vending machine fan—while speaking. Your friends will hear nothing but your beautiful voice ✨
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "AI Noise Suppression",
|
||||||
|
description: "Uses an open-source AI model (RNNoise) to remove background noise from your microphone",
|
||||||
|
authors: [Devs.Vap],
|
||||||
|
settings,
|
||||||
|
enabledByDefault: true,
|
||||||
|
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
// Pass microphone stream to RNNoise
|
||||||
|
find: "window.webkitAudioContext",
|
||||||
|
replacement: {
|
||||||
|
match: /(?<=\i\.acquire=function\((\i)\)\{return )navigator\.mediaDevices\.getUserMedia\(\1\)(?=\})/,
|
||||||
|
replace: "$&.then(stream => $self.connectRnnoise(stream, $1.audio))"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Noise suppression button in call modal
|
||||||
|
find: "renderNoiseCancellation()",
|
||||||
|
replacement: {
|
||||||
|
match: /(?<=(\i)\.jsxs?.{0,70}children:\[)(?=\i\?\i\.renderNoiseCancellation\(\))/,
|
||||||
|
replace: (_, react) => `${react}.jsx($self.NoiseSupressionButton, {}),`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Give noise suppression component a "shouldShow" prop
|
||||||
|
find: "Masks.PANEL_BUTTON",
|
||||||
|
replacement: {
|
||||||
|
match: /(?<==(\i)\.tooltipForceOpen.{0,100})(?=tooltipClassName:)/,
|
||||||
|
replace: (_, props) => `shouldShow: ${props}.shouldShow,`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Noise suppression option in voice settings
|
||||||
|
find: "Messages.USER_SETTINGS_NOISE_CANCELLATION_KRISP",
|
||||||
|
replacement: [{
|
||||||
|
match: /(?<=(\i)=\i\?\i\.KRISP:\i.{1,20}?;)/,
|
||||||
|
replace: (_, option) => `if ($self.isEnabled()) ${option} = ${JSON.stringify(RNNOISE_OPTION)};`,
|
||||||
|
}, {
|
||||||
|
match: /(?=\i&&(\i)\.push\(\{name:(?:\i\.){1,2}Messages.USER_SETTINGS_NOISE_CANCELLATION_KRISP)/,
|
||||||
|
replace: (_, options) => `${options}.push({ name: "AI (RNNoise)", value: "${RNNOISE_OPTION}" });`,
|
||||||
|
}, {
|
||||||
|
match: /(?<=onChange:function\((\i)\)\{)(?=(?:\i\.){1,2}setNoiseCancellation)/,
|
||||||
|
replace: (_, option) => `$self.setEnabled(${option}.value === ${JSON.stringify(RNNOISE_OPTION)});`,
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
setEnabled,
|
||||||
|
isEnabled: () => settings.store.isEnabled,
|
||||||
|
async connectRnnoise(stream: MediaStream, isAudio: boolean): Promise<MediaStream> {
|
||||||
|
if (!isAudio) return stream;
|
||||||
|
if (!settings.store.isEnabled) return stream;
|
||||||
|
|
||||||
|
const audioCtx = new AudioContext();
|
||||||
|
await audioCtx.audioWorklet.addModule(rnnoiseWorkletSrc);
|
||||||
|
|
||||||
|
const rnnoiseWasm = await getRnnoiseWasm();
|
||||||
|
if (!rnnoiseWasm) {
|
||||||
|
logger.warn("Failed to load RNNoise, noise suppression won't work");
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rnnoise = new RnnoiseWorkletNode(audioCtx, {
|
||||||
|
wasmBinary: rnnoiseWasm,
|
||||||
|
maxChannels: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const source = audioCtx.createMediaStreamSource(stream);
|
||||||
|
source.connect(rnnoise);
|
||||||
|
|
||||||
|
const dest = audioCtx.createMediaStreamDestination();
|
||||||
|
rnnoise.connect(dest);
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
const onEnded = () => {
|
||||||
|
rnnoise.disconnect();
|
||||||
|
source.disconnect();
|
||||||
|
audioCtx.close();
|
||||||
|
rnnoise.destroy();
|
||||||
|
};
|
||||||
|
stream.addEventListener("inactive", onEnded, { once: true });
|
||||||
|
|
||||||
|
return dest.stream;
|
||||||
|
},
|
||||||
|
NoiseSupressionButton(): ReactNode {
|
||||||
|
const { isEnabled } = settings.use();
|
||||||
|
const { isLoading, isError } = loadedStore.use();
|
||||||
|
|
||||||
|
return <Popout
|
||||||
|
key="rnnoise-popout"
|
||||||
|
align="center"
|
||||||
|
animation={Popout.Animation.TRANSLATE}
|
||||||
|
autoInvert={true}
|
||||||
|
nudgeAlignIntoViewport={true}
|
||||||
|
position="top"
|
||||||
|
renderPopout={() => <NoiseSupressionPopout />}
|
||||||
|
spacing={8}
|
||||||
|
>
|
||||||
|
{(props, { isShown }) => (
|
||||||
|
<PanelButton
|
||||||
|
{...props}
|
||||||
|
tooltipText="Noise Suppression powered by RNNoise"
|
||||||
|
tooltipClassName={cl("tooltip")}
|
||||||
|
shouldShow={!isShown}
|
||||||
|
icon={() => <div style={{
|
||||||
|
color: isError ? "var(--status-danger)" : "inherit",
|
||||||
|
opacity: isLoading ? 0.5 : 1,
|
||||||
|
}}>
|
||||||
|
<SupressionIcon enabled={isEnabled} />
|
||||||
|
</div>}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Popout>;
|
||||||
|
},
|
||||||
|
});
|
29
src/plugins/rnnoise.web/styles.css
Normal file
29
src/plugins/rnnoise.web/styles.css
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
.vc-rnnoise-popout {
|
||||||
|
background: var(--background-floating);
|
||||||
|
border-radius: 0.25em;
|
||||||
|
padding: 1em;
|
||||||
|
width: 16em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-rnnoise-popout-heading {
|
||||||
|
color: var(--text-normal);
|
||||||
|
font-weight: 500;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 1.1em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
gap: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-rnnoise-popout-desc {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.9em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-rnnoise-tooltip {
|
||||||
|
text-align: center;
|
||||||
|
}
|
5
src/plugins/searchReply/README.md
Normal file
5
src/plugins/searchReply/README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# SearchReply
|
||||||
|
|
||||||
|
Adds a reply button to search results.
|
||||||
|
|
||||||
|
![Screenshot](https://i.imgur.com/SjIEHpw.png)
|
247
src/plugins/serverProfile/GuildProfileModal.tsx
Normal file
247
src/plugins/serverProfile/GuildProfileModal.tsx
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
import { classNameFactory } from "@api/Styles";
|
||||||
|
import { openImageModal, openUserProfile } from "@utils/discord";
|
||||||
|
import { classes } from "@utils/misc";
|
||||||
|
import { ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||||
|
import { LazyComponent, useAwaiter } from "@utils/react";
|
||||||
|
import { findByCode, findByPropsLazy } from "@webpack";
|
||||||
|
import { FluxDispatcher, Forms, GuildChannelStore, GuildMemberStore, moment, Parser, PresenceStore, RelationshipStore, ScrollerThin, SnowflakeUtils, TabBar, Timestamp, useEffect, UserStore, UserUtils, useState, useStateFromStores } from "@webpack/common";
|
||||||
|
import { Guild, User } from "discord-types/general";
|
||||||
|
|
||||||
|
const IconUtils = findByPropsLazy("getGuildBannerURL");
|
||||||
|
const IconClasses = findByPropsLazy("icon", "acronym", "childWrapper");
|
||||||
|
const UserRow = LazyComponent(() => findByCode(".listDiscriminator"));
|
||||||
|
|
||||||
|
const cl = classNameFactory("vc-gp-");
|
||||||
|
|
||||||
|
export function openGuildProfileModal(guild: Guild) {
|
||||||
|
openModal(props =>
|
||||||
|
<ModalRoot {...props} size={ModalSize.MEDIUM}>
|
||||||
|
<GuildProfileModal guild={guild} />
|
||||||
|
</ModalRoot>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const enum Tabs {
|
||||||
|
ServerInfo,
|
||||||
|
Friends,
|
||||||
|
BlockedUsers
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GuildProps {
|
||||||
|
guild: Guild;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RelationshipProps extends GuildProps {
|
||||||
|
setCount(count: number): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetched = {
|
||||||
|
friends: false,
|
||||||
|
blocked: false
|
||||||
|
};
|
||||||
|
|
||||||
|
function renderTimestamp(timestamp: number) {
|
||||||
|
return (
|
||||||
|
<Timestamp timestamp={moment(timestamp)} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function GuildProfileModal({ guild }: GuildProps) {
|
||||||
|
const [friendCount, setFriendCount] = useState<number>();
|
||||||
|
const [blockedCount, setBlockedCount] = useState<number>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetched.friends = false;
|
||||||
|
fetched.blocked = false;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const [currentTab, setCurrentTab] = useState(Tabs.ServerInfo);
|
||||||
|
|
||||||
|
const bannerUrl = guild.banner && IconUtils.getGuildBannerURL({
|
||||||
|
id: guild.id,
|
||||||
|
banner: guild.banner
|
||||||
|
}, true).replace(/\?size=\d+$/, "?size=1024");
|
||||||
|
|
||||||
|
const iconUrl = guild.icon && IconUtils.getGuildIconURL({
|
||||||
|
id: guild.id,
|
||||||
|
icon: guild.icon,
|
||||||
|
canAnimate: true,
|
||||||
|
size: 512
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cl("root")}>
|
||||||
|
{bannerUrl && currentTab === Tabs.ServerInfo && (
|
||||||
|
<img
|
||||||
|
className={cl("banner")}
|
||||||
|
src={bannerUrl}
|
||||||
|
alt=""
|
||||||
|
onClick={() => openImageModal(bannerUrl)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={cl("header")}>
|
||||||
|
{guild.icon
|
||||||
|
? <img
|
||||||
|
src={iconUrl}
|
||||||
|
alt=""
|
||||||
|
onClick={() => openImageModal(iconUrl)}
|
||||||
|
/>
|
||||||
|
: <div aria-hidden className={classes(IconClasses.childWrapper, IconClasses.acronym)}>{guild.acronym}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div className={cl("name-and-description")}>
|
||||||
|
<Forms.FormTitle tag="h5" className={cl("name")}>{guild.name}</Forms.FormTitle>
|
||||||
|
{guild.description && <Forms.FormText>{guild.description}</Forms.FormText>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TabBar
|
||||||
|
type="top"
|
||||||
|
look="brand"
|
||||||
|
className={cl("tab-bar")}
|
||||||
|
selectedItem={currentTab}
|
||||||
|
onItemSelect={setCurrentTab}
|
||||||
|
>
|
||||||
|
<TabBar.Item
|
||||||
|
className={cl("tab", { selected: currentTab === Tabs.ServerInfo })}
|
||||||
|
id={Tabs.ServerInfo}
|
||||||
|
>
|
||||||
|
Server Info
|
||||||
|
</TabBar.Item>
|
||||||
|
<TabBar.Item
|
||||||
|
className={cl("tab", { selected: currentTab === Tabs.Friends })}
|
||||||
|
id={Tabs.Friends}
|
||||||
|
>
|
||||||
|
Friends{friendCount !== undefined ? ` (${friendCount})` : ""}
|
||||||
|
</TabBar.Item>
|
||||||
|
<TabBar.Item
|
||||||
|
className={cl("tab", { selected: currentTab === Tabs.BlockedUsers })}
|
||||||
|
id={Tabs.BlockedUsers}
|
||||||
|
>
|
||||||
|
Blocked Users{blockedCount !== undefined ? ` (${blockedCount})` : ""}
|
||||||
|
</TabBar.Item>
|
||||||
|
</TabBar>
|
||||||
|
|
||||||
|
<div className={cl("tab-content")}>
|
||||||
|
{currentTab === Tabs.ServerInfo && <ServerInfoTab guild={guild} />}
|
||||||
|
{currentTab === Tabs.Friends && <FriendsTab guild={guild} setCount={setFriendCount} />}
|
||||||
|
{currentTab === Tabs.BlockedUsers && <BlockedUsersTab guild={guild} setCount={setBlockedCount} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function Owner(guildId: string, owner: User) {
|
||||||
|
const guildAvatar = GuildMemberStore.getMember(guildId, owner.id)?.avatar;
|
||||||
|
const ownerAvatarUrl =
|
||||||
|
guildAvatar
|
||||||
|
? IconUtils.getGuildMemberAvatarURLSimple({
|
||||||
|
userId: owner!.id,
|
||||||
|
avatar: guildAvatar,
|
||||||
|
guildId,
|
||||||
|
canAnimate: true
|
||||||
|
}, true)
|
||||||
|
: IconUtils.getUserAvatarURL(owner, true);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cl("owner")}>
|
||||||
|
<img src={ownerAvatarUrl} alt="" onClick={() => openImageModal(ownerAvatarUrl)} />
|
||||||
|
{Parser.parse(`<@${owner.id}>`)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ServerInfoTab({ guild }: GuildProps) {
|
||||||
|
const [owner] = useAwaiter(() => UserUtils.fetchUser(guild.ownerId), {
|
||||||
|
deps: [guild.ownerId],
|
||||||
|
fallbackValue: null
|
||||||
|
});
|
||||||
|
|
||||||
|
const Fields = {
|
||||||
|
"Server Owner": owner ? Owner(guild.id, owner) : "Loading...",
|
||||||
|
"Created At": renderTimestamp(SnowflakeUtils.extractTimestamp(guild.id)),
|
||||||
|
"Joined At": renderTimestamp(guild.joinedAt.getTime()),
|
||||||
|
"Vanity Link": guild.vanityURLCode ? (<a>{`discord.gg/${guild.vanityURLCode}`}</a>) : "-", // Making the anchor href valid would cause Discord to reload
|
||||||
|
"Preferred Locale": guild.preferredLocale || "-",
|
||||||
|
"Verification Level": ["None", "Low", "Medium", "High", "Highest"][guild.verificationLevel] || "?",
|
||||||
|
"Nitro Boosts": `${guild.premiumSubscriberCount ?? 0} (Level ${guild.premiumTier ?? 0})`,
|
||||||
|
"Channels": GuildChannelStore.getChannels(guild.id)?.count - 1 || "?", // - null category
|
||||||
|
"Roles": Object.keys(guild.roles).length - 1, // - @everyone
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cl("info")}>
|
||||||
|
{Object.entries(Fields).map(([name, node]) =>
|
||||||
|
<div className={cl("server-info-pair")} key={name}>
|
||||||
|
<Forms.FormTitle tag="h5">{name}</Forms.FormTitle>
|
||||||
|
{typeof node === "string" ? <span>{node}</span> : node}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FriendsTab({ guild, setCount }: RelationshipProps) {
|
||||||
|
return UserList("friends", guild, RelationshipStore.getFriendIDs(), setCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BlockedUsersTab({ guild, setCount }: RelationshipProps) {
|
||||||
|
const blockedIds = Object.keys(RelationshipStore.getRelationships()).filter(id => RelationshipStore.isBlocked(id));
|
||||||
|
return UserList("blocked", guild, blockedIds, setCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UserList(type: "friends" | "blocked", guild: Guild, ids: string[], setCount: (count: number) => void) {
|
||||||
|
const missing = [] as string[];
|
||||||
|
const members = [] as string[];
|
||||||
|
|
||||||
|
for (const id of ids) {
|
||||||
|
if (GuildMemberStore.isMember(guild.id, id))
|
||||||
|
members.push(id);
|
||||||
|
else
|
||||||
|
missing.push(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used for side effects (rerender on member request success)
|
||||||
|
useStateFromStores(
|
||||||
|
[GuildMemberStore],
|
||||||
|
() => GuildMemberStore.getMemberIds(guild.id),
|
||||||
|
null,
|
||||||
|
(old, curr) => old.length === curr.length
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!fetched[type] && missing.length) {
|
||||||
|
fetched[type] = true;
|
||||||
|
FluxDispatcher.dispatch({
|
||||||
|
type: "GUILD_MEMBERS_REQUEST",
|
||||||
|
guildIds: [guild.id],
|
||||||
|
userIds: missing
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => setCount(members.length), [members.length]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollerThin fade className={cl("scroller")}>
|
||||||
|
{members.map(id =>
|
||||||
|
<UserRow
|
||||||
|
user={UserStore.getUser(id)}
|
||||||
|
status={PresenceStore.getStatus(id) || "offline"}
|
||||||
|
onSelect={() => openUserProfile(id)}
|
||||||
|
onContextMenu={() => { }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ScrollerThin>
|
||||||
|
);
|
||||||
|
}
|
7
src/plugins/serverProfile/README.md
Normal file
7
src/plugins/serverProfile/README.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# ServerProfile
|
||||||
|
|
||||||
|
Allows you to view info about servers and see friends and blocked users
|
||||||
|
|
||||||
|
![image](https://github.com/Vendicated/Vencord/assets/45497981/a49783b5-e8fc-41d8-968f-58600e9f6580)
|
||||||
|
![image](https://github.com/Vendicated/Vencord/assets/45497981/5efc158a-e671-4196-a15a-77edf79a2630)
|
||||||
|
![image](https://github.com/Vendicated/Vencord/assets/45497981/f43be943-6dc4-4232-9709-fbeb382d8e54)
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user