Compare commits
82 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
d9933c5793 | ||
|
2735037a67 | ||
|
83bfe28fa4 | ||
|
08c5d23636 | ||
|
ac0f834155 | ||
|
fa16e1b56f | ||
|
cfca393f2b | ||
|
eacc673bcc | ||
|
dba6c4cea6 | ||
|
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 |
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@ -38,11 +38,11 @@ jobs:
|
||||
run: pnpm build --standalone
|
||||
|
||||
- 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
|
||||
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
|
||||
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();`,
|
||||
`var .{1,2}=([^;]+);`
|
||||
- If you don't care about that part, just match a bunch of chars: `.{0,50}`, for example to extract the variable "b" in `createElement("div",{a:"foo",c:"bar"},b)`, `createElement\("div".{0,30},(.{1,2})\),`. Note the `.{0,30}`, this is essentially the same as `.+`, but safer as you can't end up accidently eating thousands of characters
|
||||
- Additionally, as you might have noticed, all of the 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"
|
||||
|
||||
|
@ -4,8 +4,7 @@
|
||||
|
||||
The cutest Discord client mod
|
||||
|
||||
![](https://user-images.githubusercontent.com/45497981/235015332-0453d3eb-1da6-4601-963e-ef5e454123a1.png)
|
||||
*A screenshot of Vencord featuring the [ClearVision-v6 theme](https://github.com/ClearVision/ClearVision-v6) (Vencord does not come with it pre-installed, it is only an example)*
|
||||
![image](https://github.com/Vendicated/Vencord/assets/45497981/706722b1-32de-4d99-bee9-93993b504334)
|
||||
|
||||
## Features
|
||||
|
||||
|
@ -19,9 +19,11 @@
|
||||
/// <reference path="../src/modules.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 { debounce } from "../src/utils";
|
||||
import { EXTENSION_BASE_URL } from "../src/utils/web-metadata";
|
||||
import { getTheme, Theme } from "../src/utils/discord";
|
||||
import { getThemeInfo } from "../src/main/themes";
|
||||
|
||||
@ -80,6 +82,7 @@ window.VencordNative = {
|
||||
return;
|
||||
}
|
||||
|
||||
win.baseUrl = EXTENSION_BASE_URL;
|
||||
win.setCss = setCssDebounced;
|
||||
win.getCurrentCss = () => VencordNative.quickCss.get();
|
||||
win.getTheme = () =>
|
||||
@ -87,7 +90,7 @@ window.VencordNative = {
|
||||
? "vs-light"
|
||||
: "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");
|
||||
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");
|
||||
style.type = "text/css";
|
||||
|
@ -28,7 +28,7 @@
|
||||
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": ["dist/Vencord.js", "dist/Vencord.css"],
|
||||
"resources": ["dist/*", "third-party/*"],
|
||||
"matches": ["*://*.discord.com/*"]
|
||||
}
|
||||
],
|
||||
|
@ -26,11 +26,7 @@
|
||||
}
|
||||
],
|
||||
|
||||
"background": {
|
||||
"scripts": ["background.js"]
|
||||
},
|
||||
|
||||
"web_accessible_resources": ["dist/Vencord.js", "dist/Vencord.css"],
|
||||
"web_accessible_resources": ["dist/*", "third-party/*"],
|
||||
|
||||
"browser_specific_settings": {
|
||||
"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.
|
||||
|
||||
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:
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "vencord",
|
||||
"private": "true",
|
||||
"version": "1.4.5",
|
||||
"version": "1.5.2",
|
||||
"description": "The cutest Discord client mod",
|
||||
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
||||
"bugs": {
|
||||
@ -36,10 +36,13 @@
|
||||
"@vap/shiki": "0.10.5",
|
||||
"eslint-plugin-simple-header": "^1.0.2",
|
||||
"fflate": "^0.7.4",
|
||||
"gifenc": "github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3",
|
||||
"monaco-editor": "^0.43.0",
|
||||
"nanoid": "^4.0.2",
|
||||
"virtual-merge": "^1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chrome": "^0.0.246",
|
||||
"@types/diff": "^5.0.3",
|
||||
"@types/lodash": "^4.14.194",
|
||||
"@types/node": "^18.16.3",
|
||||
@ -64,7 +67,8 @@
|
||||
"stylelint-config-standard": "^33.0.0",
|
||||
"tsx": "^3.12.7",
|
||||
"type-fest": "^3.9.0",
|
||||
"typescript": "^5.0.4"
|
||||
"typescript": "^5.0.4",
|
||||
"zip-local": "^0.3.5"
|
||||
},
|
||||
"packageManager": "pnpm@8.1.1",
|
||||
"pnpm": {
|
||||
|
81
pnpm-lock.yaml
generated
81
pnpm-lock.yaml
generated
@ -1,5 +1,9 @@
|
||||
lockfileVersion: '6.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
patchedDependencies:
|
||||
eslint-plugin-path-alias@1.0.0:
|
||||
hash: m6sma4g6bh67km3q6igf6uxaja
|
||||
@ -24,6 +28,12 @@ dependencies:
|
||||
fflate:
|
||||
specifier: ^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:
|
||||
specifier: ^4.0.2
|
||||
version: 4.0.2
|
||||
@ -32,6 +42,9 @@ dependencies:
|
||||
version: 1.0.1
|
||||
|
||||
devDependencies:
|
||||
'@types/chrome':
|
||||
specifier: ^0.0.246
|
||||
version: 0.0.246
|
||||
'@types/diff':
|
||||
specifier: ^5.0.3
|
||||
version: 5.0.3
|
||||
@ -107,6 +120,9 @@ devDependencies:
|
||||
typescript:
|
||||
specifier: ^5.0.4
|
||||
version: 5.0.4
|
||||
zip-local:
|
||||
specifier: ^0.3.5
|
||||
version: 0.3.5
|
||||
|
||||
packages:
|
||||
|
||||
@ -520,10 +536,31 @@ packages:
|
||||
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:
|
||||
resolution: {integrity: sha512-amrLbRqTU9bXMCc6uX0sWpxsQzRIo9z6MJPkH1pkez/qOxuqSZVuryJAWoBRq94CeG8JxY+VK4Le9HtjQR5T9A==}
|
||||
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:
|
||||
resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==}
|
||||
dev: true
|
||||
@ -843,6 +880,10 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/async@1.5.2:
|
||||
resolution: {integrity: sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==}
|
||||
dev: true
|
||||
|
||||
/atob@2.1.2:
|
||||
resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==}
|
||||
engines: {node: '>= 4.5.0'}
|
||||
@ -1867,6 +1908,10 @@ packages:
|
||||
resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==}
|
||||
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:
|
||||
resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==}
|
||||
dev: true
|
||||
@ -2184,6 +2229,12 @@ packages:
|
||||
resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
|
||||
dev: false
|
||||
|
||||
/jszip@2.7.0:
|
||||
resolution: {integrity: sha512-JIsRKRVC3gTRo2vM4Wy9WBC3TRcfnIZU8k65Phi3izkvPH975FowRYtKGT6PxevA0XnJ/yO8b0QwV0ydVyQwfw==}
|
||||
dependencies:
|
||||
pako: 1.0.11
|
||||
dev: true
|
||||
|
||||
/kind-of@3.2.2:
|
||||
resolution: {integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -2354,6 +2405,10 @@ packages:
|
||||
resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==}
|
||||
dev: true
|
||||
|
||||
/monaco-editor@0.43.0:
|
||||
resolution: {integrity: sha512-cnoqwQi/9fml2Szamv1XbSJieGJ1Dc8tENVMD26Kcfl7xGQWp7OBKMjlwKVGYFJ3/AXJjSOGvcqK7Ry/j9BM1Q==}
|
||||
dev: false
|
||||
|
||||
/ms@2.0.0:
|
||||
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
||||
dev: true
|
||||
@ -2511,6 +2566,10 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
dev: true
|
||||
|
||||
/pako@1.0.11:
|
||||
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
|
||||
dev: true
|
||||
|
||||
/parent-module@1.0.1:
|
||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||
engines: {node: '>=6'}
|
||||
@ -2662,6 +2721,11 @@ packages:
|
||||
- utf-8-validate
|
||||
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:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
dev: true
|
||||
@ -3377,6 +3441,17 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
/zip-local@0.3.5:
|
||||
resolution: {integrity: sha512-GRV3D5TJY+/PqyeRm5CYBs7xVrKTKzljBoEXvocZu0HJ7tPEcgpSOYa2zFIsCZWgKWMuc4U3yMFgFkERGFIB9w==}
|
||||
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,16 @@
|
||||
|
||||
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 = {
|
||||
IS_STANDALONE: isStandalone,
|
||||
IS_DEV: JSON.stringify(watch),
|
||||
IS_UPDATER_DISABLED: updaterDisabled,
|
||||
IS_WEB: false,
|
||||
IS_EXTENSION: false,
|
||||
VERSION: JSON.stringify(VERSION),
|
||||
BUILD_TIMESTAMP: Date.now(),
|
||||
BUILD_TIMESTAMP,
|
||||
};
|
||||
if (defines.IS_STANDALONE === "false")
|
||||
// If this is a local build (not standalone), optimise
|
||||
@ -76,7 +79,6 @@ await Promise.all([
|
||||
],
|
||||
define: {
|
||||
...defines,
|
||||
IS_WEB: false,
|
||||
IS_DISCORD_DESKTOP: true,
|
||||
IS_VESKTOP: false
|
||||
}
|
||||
@ -122,7 +124,6 @@ await Promise.all([
|
||||
],
|
||||
define: {
|
||||
...defines,
|
||||
IS_WEB: false,
|
||||
IS_DISCORD_DESKTOP: false,
|
||||
IS_VESKTOP: true
|
||||
}
|
||||
|
@ -17,14 +17,13 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
import esbuild from "esbuild";
|
||||
import { zip } from "fflate";
|
||||
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 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}
|
||||
@ -42,28 +41,69 @@ const commonOptions = {
|
||||
target: ["esnext"],
|
||||
define: {
|
||||
IS_WEB: "true",
|
||||
IS_EXTENSION: "false",
|
||||
IS_STANDALONE: "true",
|
||||
IS_DEV: JSON.stringify(watch),
|
||||
IS_DISCORD_DESKTOP: "false",
|
||||
IS_VESKTOP: "false",
|
||||
IS_UPDATER_DISABLED: "true",
|
||||
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(
|
||||
[
|
||||
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({
|
||||
...commonOptions,
|
||||
outfile: "dist/browser.js",
|
||||
footer: { js: "//# sourceURL=VencordWeb" },
|
||||
}),
|
||||
esbuild.build({
|
||||
...commonOptions,
|
||||
outfile: "dist/extension.js",
|
||||
define: {
|
||||
...commonOptions?.define,
|
||||
IS_EXTENSION: "true",
|
||||
},
|
||||
footer: { js: "//# sourceURL=VencordWeb" },
|
||||
}),
|
||||
esbuild.build({
|
||||
...commonOptions,
|
||||
inject: ["browser/GMPolyfill.js", ...(commonOptions?.inject || [])],
|
||||
define: {
|
||||
"window": "unsafeWindow",
|
||||
...(commonOptions?.define)
|
||||
...(commonOptions?.define),
|
||||
window: "unsafeWindow",
|
||||
},
|
||||
outfile: "dist/Vencord.user.js",
|
||||
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 = {
|
||||
"dist/Vencord.js": await readFile("dist/browser.js"),
|
||||
"dist/Vencord.css": await readFile("dist/browser.css"),
|
||||
"dist/Vencord.js": await readFile("dist/extension.js"),
|
||||
"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 => {
|
||||
let content = await readFile(join("browser", f));
|
||||
if (f.startsWith("manifest")) {
|
||||
@ -99,31 +168,15 @@ 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 Promise.all(Object.entries(entries).map(async ([file, content]) => {
|
||||
const dest = join("dist", target, file);
|
||||
const parentDirectory = join(dest, "..");
|
||||
await mkdir(parentDirectory, { recursive: true });
|
||||
await writeFile(dest, content);
|
||||
}));
|
||||
await rm(target, { recursive: true, force: true });
|
||||
await Promise.all(Object.entries(entries).map(async ([file, content]) => {
|
||||
const dest = join("dist", target, file);
|
||||
const parentDirectory = join(dest, "..");
|
||||
await mkdir(parentDirectory, { recursive: true });
|
||||
await writeFile(dest, content);
|
||||
}));
|
||||
|
||||
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 => {
|
||||
@ -141,8 +194,9 @@ const appendCssRuntime = readFile("dist/Vencord.user.css", "utf-8").then(content
|
||||
|
||||
await Promise.all([
|
||||
appendCssRuntime,
|
||||
buildPluginZip("extension.zip", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"], true),
|
||||
buildPluginZip("chromium-unpacked", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"], false),
|
||||
buildPluginZip("firefox-unpacked", ["background.js", "content.js", "manifestv2.json", "icon.png"], false),
|
||||
buildExtension("chromium-unpacked", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"]),
|
||||
buildExtension("firefox-unpacked", ["content.js", "manifestv2.json", "icon.png"], true),
|
||||
]);
|
||||
|
||||
Zip.sync.zip("dist/chromium-unpacked").compress().save("dist/extension.zip");
|
||||
console.info("Packed Chromium Extension written to dist/extension.zip");
|
||||
|
@ -30,15 +30,18 @@ import PackageJSON from "../../package.json" assert { type: "json" };
|
||||
import { getPluginTarget } from "../utils.mjs";
|
||||
|
||||
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 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 = {
|
||||
js: `
|
||||
// Vencord ${gitHash}
|
||||
// Standalone: ${isStandalone}
|
||||
// Platform: ${isStandalone === "false" ? process.platform : "Universal"}
|
||||
// Updater disabled: ${updaterDisabled}
|
||||
`.trim()
|
||||
};
|
||||
|
||||
@ -133,11 +136,14 @@ export const gitRemotePlugin = {
|
||||
namespace: "git-remote", path: args.path
|
||||
}));
|
||||
build.onLoad({ filter, namespace: "git-remote" }, async () => {
|
||||
const res = await promisify(exec)("git remote get-url origin", { encoding: "utf-8" });
|
||||
const remote = res.stdout.trim()
|
||||
.replace("https://github.com/", "")
|
||||
.replace("git@github.com:", "")
|
||||
.replace(/.git$/, "");
|
||||
let remote = process.env.VENCORD_REMOTE;
|
||||
if (!remote) {
|
||||
const res = await promisify(exec)("git remote get-url origin", { encoding: "utf-8" });
|
||||
remote = res.stdout.trim()
|
||||
.replace("https://github.com/", "")
|
||||
.replace("git@github.com:", "")
|
||||
.replace(/.git$/, "");
|
||||
}
|
||||
|
||||
return { contents: `export default "${remote}"` };
|
||||
});
|
||||
|
@ -18,7 +18,8 @@
|
||||
|
||||
import { Dirent, readdirSync, readFileSync, writeFileSync } from "fs";
|
||||
import { access, readFile } from "fs/promises";
|
||||
import { join } from "path";
|
||||
import { join, sep } from "path";
|
||||
import { normalize as posixNormalize, sep as posixSep } from "path/posix";
|
||||
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";
|
||||
@ -39,6 +40,7 @@ interface PluginData {
|
||||
required: boolean;
|
||||
enabledByDefault: boolean;
|
||||
target: "discordDesktop" | "vencordDesktop" | "web" | "dev";
|
||||
filePath: string;
|
||||
}
|
||||
|
||||
const devs = {} as Record<string, Dev>;
|
||||
@ -161,11 +163,21 @@ async function parseFile(fileName: string) {
|
||||
|
||||
const target = getPluginTarget(fileName);
|
||||
if (target) {
|
||||
if (!["web", "discordDesktop", "vencordDesktop", "dev"].includes(target)) throw fail(`invalid target ${target}`);
|
||||
if (!["web", "discordDesktop", "vencordDesktop", "desktop", "dev"].includes(target)) throw fail(`invalid target ${target}`);
|
||||
data.target = target as any;
|
||||
}
|
||||
|
||||
return data;
|
||||
data.filePath = posixNormalize(fileName)
|
||||
.split(sep)
|
||||
.join(posixSep)
|
||||
.replace(/\/index\.([jt]sx?)$/, "")
|
||||
.replace(/^src\/plugins\//, "");
|
||||
|
||||
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");
|
||||
@ -194,18 +206,24 @@ function isPluginFile({ name }: { name: string; }) {
|
||||
(async () => {
|
||||
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 })
|
||||
.filter(isPluginFile)
|
||||
.map(async dirent =>
|
||||
parseFile(await getEntryPoint(dir, dirent))
|
||||
)
|
||||
);
|
||||
.map(async 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[3], JSON.stringify(readmes));
|
||||
} else {
|
||||
console.log(data);
|
||||
}
|
||||
|
@ -171,7 +171,7 @@ page.on("console", async e => {
|
||||
plugin,
|
||||
type,
|
||||
id,
|
||||
match: regex,
|
||||
match: regex.replace(/\[A-Za-z_\$\]\[\\w\$\]\*/g, "\\i"),
|
||||
error: cause
|
||||
});
|
||||
break;
|
||||
|
@ -22,7 +22,7 @@
|
||||
*/
|
||||
export function getPluginTarget(filePath) {
|
||||
const pathParts = filePath.split(/[/\\]/);
|
||||
if (/^index\.tsx?$/.test(filePath.at(-1))) pathParts.pop();
|
||||
if (/^index\.tsx?$/.test(pathParts.at(-1))) pathParts.pop();
|
||||
|
||||
const identifier = pathParts.at(-1).replace(/\.tsx?$/, "");
|
||||
const identiferBits = identifier.split(".");
|
||||
|
@ -48,7 +48,7 @@ export default {
|
||||
},
|
||||
|
||||
addThemeChangeListener(cb: () => void) {
|
||||
ipcRenderer.on(IpcEvents.THEME_UPDATE, cb);
|
||||
ipcRenderer.on(IpcEvents.THEME_UPDATE, () => cb());
|
||||
},
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
@ -28,8 +28,8 @@ interface BaseIconProps extends IconProps {
|
||||
|
||||
interface IconProps extends SVGProps<SVGSVGElement> {
|
||||
className?: string;
|
||||
height?: number;
|
||||
width?: number;
|
||||
height?: string | number;
|
||||
width?: string | number;
|
||||
}
|
||||
|
||||
function Icon({ height = 24, width = 24, className, children, viewBox, ...svgProps }: PropsWithChildren<BaseIconProps>) {
|
||||
@ -97,7 +97,7 @@ export function OpenExternalIcon(props: IconProps) {
|
||||
>
|
||||
<polygon
|
||||
fill="currentColor"
|
||||
fill-rule="nonzero"
|
||||
fillRule="nonzero"
|
||||
points="13 20 11 20 11 8 5.5 13.5 4.08 12.08 12 4.16 19.92 12.08 18.5 13.5 13 8"
|
||||
/>
|
||||
</Icon>
|
||||
@ -121,9 +121,13 @@ export function InfoIcon(props: IconProps) {
|
||||
<Icon
|
||||
{...props}
|
||||
className={classes(props.className, "vc-info-icon")}
|
||||
viewBox="0 0 12 12"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path fill="currentColor" d="M6 1C3.243 1 1 3.244 1 6c0 2.758 2.243 5 5 5s5-2.242 5-5c0-2.756-2.243-5-5-5zm0 2.376a.625.625 0 110 1.25.625.625 0 010-1.25zM7.5 8.5h-3v-1h1V6H5V5h1a.5.5 0 01.5.5v2h1v1z" />
|
||||
<path
|
||||
fill="currentColor"
|
||||
transform="translate(2 2)"
|
||||
d="M9,7 L11,7 L11,5 L9,5 L9,7 Z M10,18 C5.59,18 2,14.41 2,10 C2,5.59 5.59,2 10,2 C14.41,2 18,5.59 18,10 C18,14.41 14.41,18 10,18 L10,18 Z M10,4.4408921e-16 C4.4771525,-1.77635684e-15 4.4408921e-16,4.4771525 0,10 C-1.33226763e-15,12.6521649 1.0535684,15.195704 2.92893219,17.0710678 C4.80429597,18.9464316 7.3478351,20 10,20 C12.6521649,20 15.195704,18.9464316 17.0710678,17.0710678 C18.9464316,15.195704 20,12.6521649 20,10 C20,7.3478351 18.9464316,4.80429597 17.0710678,2.92893219 C15.195704,1.0535684 12.6521649,2.22044605e-16 10,0 L10,4.4408921e-16 Z M9,15 L11,15 L11,9 L9,9 L9,15 L9,15 Z"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
@ -139,8 +143,8 @@ export function OwnerCrownIcon(props: IconProps) {
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M13.6572 5.42868C13.8879 5.29002 14.1806 5.30402 14.3973 5.46468C14.6133 5.62602 14.7119 5.90068 14.6473 6.16202L13.3139 11.4954C13.2393 11.7927 12.9726 12.0007 12.6666 12.0007H3.33325C3.02725 12.0007 2.76058 11.792 2.68592 11.4954L1.35258 6.16202C1.28792 5.90068 1.38658 5.62602 1.60258 5.46468C1.81992 5.30468 2.11192 5.29068 2.34325 5.42868L5.13192 7.10202L7.44592 3.63068C7.46173 3.60697 7.48377 3.5913 7.50588 3.57559C7.5192 3.56612 7.53255 3.55663 7.54458 3.54535L6.90258 2.90268C6.77325 2.77335 6.77325 2.56068 6.90258 2.43135L7.76458 1.56935C7.89392 1.44002 8.10658 1.44002 8.23592 1.56935L9.09792 2.43135C9.22725 2.56068 9.22725 2.77335 9.09792 2.90268L8.45592 3.54535C8.46794 3.55686 8.48154 3.56651 8.49516 3.57618C8.51703 3.5917 8.53897 3.60727 8.55458 3.63068L10.8686 7.10202L13.6572 5.42868ZM2.66667 12.6673H13.3333V14.0007H2.66667V12.6673Z"
|
||||
/>
|
||||
</Icon>
|
||||
@ -159,8 +163,6 @@ export function ScreenshareIcon(props: IconProps) {
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M2 4.5C2 3.397 2.897 2.5 4 2.5H20C21.103 2.5 22 3.397 22 4.5V15.5C22 16.604 21.103 17.5 20 17.5H13V19.5H17V21.5H7V19.5H11V17.5H4C2.897 17.5 2 16.604 2 15.5V4.5ZM13.2 14.3375V11.6C9.864 11.6 7.668 12.6625 6 15C6.672 11.6625 8.532 8.3375 13.2 7.6625V5L18 9.6625L13.2 14.3375Z"
|
||||
/>
|
||||
</Icon>
|
||||
@ -198,8 +200,24 @@ export function Microphone(props: IconProps) {
|
||||
className={classes(props.className, "vc-microphone")}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.99 11C14.99 12.66 13.66 14 12 14C10.34 14 9 12.66 9 11V5C9 3.34 10.34 2 12 2C13.66 2 15 3.34 15 5L14.99 11ZM12 16.1C14.76 16.1 17.3 14 17.3 11H19C19 14.42 16.28 17.24 13 17.72V21H11V17.72C7.72 17.23 5 14.41 5 11H6.7C6.7 14 9.24 16.1 12 16.1ZM12 4C11.2 4 11 4.66667 11 5V11C11 11.3333 11.2 12 12 12C12.8 12 13 11.3333 13 11V5C13 4.66667 12.8 4 12 4Z" fill="currentColor" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.99 11C14.99 12.66 13.66 14 12 14C10.34 14 9 12.66 9 11V5C9 3.34 10.34 2 12 2C13.66 2 15 3.34 15 5L14.99 11ZM12 16.1C14.76 16.1 17.3 14 17.3 11H19C19 14.42 16.28 17.24 13 17.72V22H11V17.72C7.72 17.23 5 14.41 5 11H6.7C6.7 14 9.24 16.1 12 16.1Z" fill="currentColor" />
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M14.99 11C14.99 12.66 13.66 14 12 14C10.34 14 9 12.66 9 11V5C9 3.34 10.34 2 12 2C13.66 2 15 3.34 15 5L14.99 11ZM12 16.1C14.76 16.1 17.3 14 17.3 11H19C19 14.42 16.28 17.24 13 17.72V21H11V17.72C7.72 17.23 5 14.41 5 11H6.7C6.7 14 9.24 16.1 12 16.1ZM12 4C11.2 4 11 4.66667 11 5V11C11 11.3333 11.2 12 12 12C12.8 12 13 11.3333 13 11V5C13 4.66667 12.8 4 12 4Z" fill="currentColor" />
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M14.99 11C14.99 12.66 13.66 14 12 14C10.34 14 9 12.66 9 11V5C9 3.34 10.34 2 12 2C13.66 2 15 3.34 15 5L14.99 11ZM12 16.1C14.76 16.1 17.3 14 17.3 11H19C19 14.42 16.28 17.24 13 17.72V22H11V17.72C7.72 17.23 5 14.41 5 11H6.7C6.7 14 9.24 16.1 12 16.1Z" fill="currentColor" />
|
||||
</Icon >
|
||||
);
|
||||
}
|
||||
|
||||
export function CogWheel(props: IconProps) {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
className={classes(props.className, "vc-cog-wheel")}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
fill="currentColor"
|
||||
d="M19.738 10H22V14H19.739C19.498 14.931 19.1 15.798 18.565 16.564L20 18L18 20L16.565 18.564C15.797 19.099 14.932 19.498 14 19.738V22H10V19.738C9.069 19.498 8.203 19.099 7.436 18.564L6 20L4 18L5.436 16.564C4.901 15.799 4.502 14.932 4.262 14H2V10H4.262C4.502 9.068 4.9 8.202 5.436 7.436L4 6L6 4L7.436 5.436C8.202 4.9 9.068 4.502 10 4.262V2H14V4.261C14.932 4.502 15.797 4.9 16.565 5.435L18 3.999L20 5.999L18.564 7.436C19.099 8.202 19.498 9.069 19.738 10ZM12 16C14.2091 16 16 14.2091 16 12C16 9.79086 14.2091 8 12 8C9.79086 8 8 9.79086 8 12C8 14.2091 9.79086 16 12 16Z"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
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 { useSettings } from "@api/Settings";
|
||||
import { disableStyle, enableStyle } from "@api/Styles";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { proxyLazy } from "@utils/lazy";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes } from "@utils/misc";
|
||||
import { classes, isObjectEmpty } from "@utils/misc";
|
||||
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
|
||||
import { LazyComponent } from "@utils/react";
|
||||
import { OptionType, Plugin } from "@utils/types";
|
||||
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 { Constructor } from "type-fest";
|
||||
|
||||
@ -41,7 +40,7 @@ import {
|
||||
SettingSliderComponent,
|
||||
SettingTextComponent
|
||||
} from "./components";
|
||||
import hideBotTagStyle from "./userPopoutHideBotTag.css?managed";
|
||||
import { openContributorModal } from "./ContributorModal";
|
||||
|
||||
const UserSummaryItem = LazyComponent(() => findByCode("defaultRenderUser", "showDefaultAvatarsForNullUsers"));
|
||||
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 hasSettings = Boolean(pluginSettings && plugin.options);
|
||||
const hasSettings = Boolean(pluginSettings && plugin.options && !isObjectEmpty(plugin.options));
|
||||
|
||||
React.useEffect(() => {
|
||||
enableStyle(hideBotTagStyle);
|
||||
|
||||
let originalUser: User;
|
||||
(async () => {
|
||||
for (const user of plugin.authors.slice(0, 6)) {
|
||||
const author = 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 }))
|
||||
: makeDummyUser({ username: user.name });
|
||||
|
||||
setAuthors(a => [...a, author]);
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
disableStyle(hideBotTagStyle);
|
||||
if (originalUser)
|
||||
FluxDispatcher.dispatch({ type: "USER_UPDATE", user: originalUser });
|
||||
};
|
||||
}, []);
|
||||
|
||||
async function saveAndClose() {
|
||||
@ -214,6 +202,19 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
||||
showDefaultAvatarsForNullUsers
|
||||
showUserPopout
|
||||
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>
|
||||
</Forms.FormSection>
|
||||
|
@ -33,11 +33,10 @@ export function SettingTextComponent({ option, pluginSettings, definedSettings,
|
||||
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
||||
if (typeof isValid === "string") setError(isValid);
|
||||
else if (!isValid) setError("Invalid input provided.");
|
||||
else {
|
||||
setError(null);
|
||||
setState(newValue);
|
||||
onChange(newValue);
|
||||
}
|
||||
else setError(null);
|
||||
|
||||
setState(newValue);
|
||||
onChange(newValue);
|
||||
}
|
||||
|
||||
return (
|
||||
|
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;
|
||||
}
|
@ -22,17 +22,18 @@ import * as DataStore from "@api/DataStore";
|
||||
import { showNotice } from "@api/Notices";
|
||||
import { Settings, useSettings } from "@api/Settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { CogWheel, InfoIcon } from "@components/Icons";
|
||||
import PluginModal from "@components/PluginSettings/PluginModal";
|
||||
import { AddonCard } from "@components/VencordSettings/AddonCard";
|
||||
import { SettingsTab } from "@components/VencordSettings/shared";
|
||||
import { ChangeList } from "@utils/ChangeList";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes } from "@utils/misc";
|
||||
import { classes, isObjectEmpty } from "@utils/misc";
|
||||
import { openModalLazy } from "@utils/modal";
|
||||
import { LazyComponent, useAwaiter } from "@utils/react";
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import { Plugin } from "@utils/types";
|
||||
import { findByCode, findByPropsLazy } from "@webpack";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { Alerts, Button, Card, Forms, Parser, React, Select, Text, TextInput, Toasts, Tooltip } from "@webpack/common";
|
||||
|
||||
import Plugins from "~plugins";
|
||||
@ -46,8 +47,6 @@ const logger = new Logger("PluginSettings", "#a6d189");
|
||||
const InputStyles = findByPropsLazy("inputDefault", "inputWrapper");
|
||||
const ButtonClasses = findByPropsLazy("button", "disabled", "enabled");
|
||||
|
||||
const CogWheel = LazyComponent(() => findByCode("18.564C15.797 19.099 14.932 19.498 14 19.738V22H10V19.738C9.069"));
|
||||
const InfoIcon = LazyComponent(() => findByCode("4.4408921e-16 C4.4771525,-1.77635684e-15 4.4408921e-16"));
|
||||
|
||||
function showErrorToast(message: string) {
|
||||
Toasts.show({
|
||||
@ -91,7 +90,7 @@ interface PluginCardProps extends React.HTMLProps<HTMLDivElement> {
|
||||
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 isEnabled = () => settings.enabled ?? false;
|
||||
@ -161,9 +160,9 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe
|
||||
onMouseLeave={onMouseLeave}
|
||||
infoButton={
|
||||
<button role="switch" onClick={() => openModal()} className={classes(ButtonClasses.button, cl("info-button"))}>
|
||||
{plugin.options
|
||||
{plugin.options && !isObjectEmpty(plugin.options)
|
||||
? <CogWheel />
|
||||
: <InfoIcon width="24" height="24" />}
|
||||
: <InfoIcon />}
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
|
@ -1,3 +0,0 @@
|
||||
[class|="userPopoutOuter"] [class*="botTag"] {
|
||||
display: none;
|
||||
}
|
@ -17,6 +17,7 @@
|
||||
*/
|
||||
|
||||
import { CheckedTextInput } from "@components/CheckedTextInput";
|
||||
import { CodeBlock } from "@components/CodeBlock";
|
||||
import { debounce } from "@utils/debounce";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
|
||||
@ -299,7 +300,7 @@ function PatchHelper() {
|
||||
{!!(find && match && replacement) && (
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
|
@ -18,8 +18,10 @@
|
||||
|
||||
import { useSettings } from "@api/Settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { ErrorCard } from "@components/ErrorCard";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { Link } from "@components/Link";
|
||||
import { IsFirefox } from "@utils/constants";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes } from "@utils/misc";
|
||||
import { showItemInFolder } from "@utils/native";
|
||||
@ -231,7 +233,7 @@ function ThemesTab() {
|
||||
ref={fileInputRef}
|
||||
onChange={onFileUpload}
|
||||
multiple={true}
|
||||
filters={[{ extensions: ["*.css"] }]}
|
||||
filters={[{ extensions: ["css"] }]}
|
||||
/>
|
||||
</Button>
|
||||
) : (
|
||||
@ -249,12 +251,14 @@ function ThemesTab() {
|
||||
>
|
||||
Load missing Themes
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => VencordNative.quickCss.openEditor()}
|
||||
size={Button.Sizes.SMALL}
|
||||
>
|
||||
Edit QuickCSS
|
||||
</Button>
|
||||
{!IsFirefox && (
|
||||
<Button
|
||||
onClick={() => VencordNative.quickCss.openEditor()}
|
||||
size={Button.Sizes.SMALL}
|
||||
>
|
||||
Edit QuickCSS
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
</Card>
|
||||
|
||||
@ -316,6 +320,15 @@ function ThemesTab() {
|
||||
|
||||
return (
|
||||
<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
|
||||
type="top"
|
||||
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 DonateButton from "@components/DonateButton";
|
||||
import { ErrorCard } from "@components/ErrorCard";
|
||||
import { IsFirefox } from "@utils/constants";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { identity } from "@utils/misc";
|
||||
import { relaunch, showItemInFolder } from "@utils/native";
|
||||
@ -109,12 +110,14 @@ function VencordSettings() {
|
||||
Restart Client
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => VencordNative.quickCss.openEditor()}
|
||||
size={Button.Sizes.SMALL}
|
||||
disabled={settingsDir === "Loading..."}>
|
||||
Open QuickCSS File
|
||||
</Button>
|
||||
{!IsFirefox && (
|
||||
<Button
|
||||
onClick={() => VencordNative.quickCss.openEditor()}
|
||||
size={Button.Sizes.SMALL}
|
||||
disabled={settingsDir === "Loading..."}>
|
||||
Open QuickCSS File
|
||||
</Button>
|
||||
)}
|
||||
{!IS_WEB && (
|
||||
<Button
|
||||
onClick={() => showItemInFolder(settingsDir)}
|
||||
@ -257,7 +260,11 @@ function DonateCard({ image }: DonateCardProps) {
|
||||
src={image}
|
||||
alt=""
|
||||
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>
|
||||
);
|
||||
|
@ -8,6 +8,7 @@
|
||||
width: 100%;
|
||||
transition: 0.1s ease-out;
|
||||
transition-property: box-shadow, transform, background, opacity;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.vc-addon-card-disabled {
|
||||
|
2
src/globals.d.ts
vendored
2
src/globals.d.ts
vendored
@ -33,8 +33,10 @@ declare global {
|
||||
* replace: `${IS_WEB}?foo:bar`
|
||||
*/
|
||||
export var IS_WEB: boolean;
|
||||
export var IS_EXTENSION: boolean;
|
||||
export var IS_DEV: boolean;
|
||||
export var IS_STANDALONE: boolean;
|
||||
export var IS_UPDATER_DISABLED: boolean;
|
||||
export var IS_DISCORD_DESKTOP: boolean;
|
||||
export var IS_VESKTOP: boolean;
|
||||
export var VERSION: string;
|
||||
|
@ -27,7 +27,7 @@ import { mkdirSync, readFileSync, watch } from "fs";
|
||||
import { open, readdir, readFile, writeFile } from "fs/promises";
|
||||
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 { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, SETTINGS_FILE, THEMES_DIR } from "./utils/constants";
|
||||
|
@ -22,6 +22,28 @@ import { readFile } from "fs/promises";
|
||||
import { request } from "https";
|
||||
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
|
||||
// These links don't support CORS, so this has to be native
|
||||
const validRedirectUrls = /^https:\/\/(spotify\.link|s\.team)\/.+$/;
|
||||
|
@ -20,18 +20,6 @@ import { app } from "electron";
|
||||
import { existsSync, mkdirSync, readdirSync, renameSync, statSync, writeFileSync } from "fs";
|
||||
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) {
|
||||
const newParts = $new.slice(4).split(".").map(Number);
|
||||
const oldParts = old.slice(4).split(".").map(Number);
|
||||
@ -77,25 +65,6 @@ function patchLatest() {
|
||||
}
|
||||
}
|
||||
|
||||
// Windows Host Updates install to a new folder app-{HOST_VERSION}, so we
|
||||
// need to reinject
|
||||
function patchUpdater() {
|
||||
// Array of autoStart paths to try
|
||||
const autoStartPaths = [
|
||||
join(require.main!.filename, "..", "autoStart", "win32.js"), // Vanilla
|
||||
join(require.main!.filename, "..", "autoStart.js") // OpenAsar
|
||||
];
|
||||
|
||||
for (const path of autoStartPaths) {
|
||||
try {
|
||||
const { update } = require(path);
|
||||
|
||||
require.cache[path]!.exports.update = function () {
|
||||
update.apply(this, arguments);
|
||||
patchLatest();
|
||||
};
|
||||
} catch {
|
||||
// Ignore as non-critical
|
||||
}
|
||||
}
|
||||
}
|
||||
// Try to patch latest on before-quit
|
||||
// Discord's Win32 updater will call app.quit() on restart and open new version on will-quit
|
||||
app.on("before-quit", patchLatest);
|
||||
|
@ -16,4 +16,5 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import(IS_STANDALONE ? "./http" : "./git");
|
||||
if (!IS_UPDATER_DISABLED)
|
||||
import(IS_STANDALONE ? "./http" : "./git");
|
||||
|
@ -103,7 +103,7 @@ export default definePlugin({
|
||||
element: require("@components/VencordSettings/ThemesTab").default,
|
||||
className: "vc-themes"
|
||||
},
|
||||
!IS_WEB && {
|
||||
!IS_UPDATER_DISABLED && {
|
||||
section: "VencordUpdater",
|
||||
label: "Updater",
|
||||
element: require("@components/VencordSettings/UpdaterTab").default,
|
||||
|
@ -17,7 +17,7 @@
|
||||
*/
|
||||
|
||||
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 { makeCodeblock } from "@utils/text";
|
||||
import definePlugin from "@utils/types";
|
||||
@ -27,9 +27,10 @@ import { Alerts, Forms, UserStore } from "@webpack/common";
|
||||
import gitHash from "~git-hash";
|
||||
import plugins from "~plugins";
|
||||
|
||||
import settings from "./_core/settings";
|
||||
import settings from "./settings";
|
||||
|
||||
const REMEMBER_DISMISS_KEY = "Vencord-SupportHelper-Dismiss";
|
||||
const FIREFOX_DISMISS_KEY = "Vencord-Firefox-Warning-Dismiss";
|
||||
|
||||
const AllowedChannelIds = [
|
||||
SUPPORT_CHANNEL_ID,
|
||||
@ -48,7 +49,7 @@ export default definePlugin({
|
||||
name: "vencord-debug",
|
||||
description: "Send Vencord Debug info",
|
||||
predicate: ctx => AllowedChannelIds.includes(ctx.channel.id),
|
||||
execute() {
|
||||
async execute() {
|
||||
const { RELEASE_CHANNEL } = window.GLOBAL_ENV;
|
||||
|
||||
const client = (() => {
|
||||
@ -75,6 +76,10 @@ export default definePlugin({
|
||||
OpenAsar: "openasar" in window,
|
||||
};
|
||||
|
||||
if (IS_DISCORD_DESKTOP) {
|
||||
info["Last Crash Reason"] = (await DiscordNative.processUtils.getLastCrash())?.rendererCrashReason ?? "N/A";
|
||||
}
|
||||
|
||||
const debugInfo = `
|
||||
**Vencord Debug Info**
|
||||
>>> ${Object.entries(info).map(([k, v]) => `${k}: ${v}`).join("\n")}
|
||||
@ -111,6 +116,22 @@ ${makeCodeblock(enabledPlugins.join(", ") + "\n\n" + enabledApiPlugins.join(", "
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
@ -23,7 +23,7 @@ import { Clipboard, Toasts } from "@webpack/common";
|
||||
|
||||
export default definePlugin({
|
||||
name: "BetterRoleDot",
|
||||
authors: [Devs.Ven],
|
||||
authors: [Devs.Ven, Devs.AutumnVN],
|
||||
description:
|
||||
"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,
|
||||
replace: "true",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
find: ".ADD_ROLE_A11Y_LABEL",
|
||||
predicate: () => Settings.plugins.BetterRoleDot.copyRoleColorInProfilePopout && !Settings.plugins.BetterRoleDot.bothStyles,
|
||||
replacement: {
|
||||
match: /"dot"===\i/,
|
||||
replace: "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: ".roleVerifiedIcon",
|
||||
predicate: () => Settings.plugins.BetterRoleDot.copyRoleColorInProfilePopout && !Settings.plugins.BetterRoleDot.bothStyles,
|
||||
replacement: {
|
||||
match: /"dot"===\i/,
|
||||
replace: "true"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@ -50,7 +67,14 @@ export default definePlugin({
|
||||
bothStyles: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Show both role dot and coloured names",
|
||||
restartNeeded: true,
|
||||
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':'')"
|
||||
}, {
|
||||
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.*",
|
||||
"feature@youtube.com",
|
||||
"kw@youtube.com",
|
||||
"si@youtube.com",
|
||||
"pp@youtube.com",
|
||||
"si@youtu.be",
|
||||
"wt_zmc",
|
||||
"utm_source",
|
||||
"utm_content",
|
||||
@ -135,4 +138,5 @@ export const defaultRules = [
|
||||
"utm_term",
|
||||
"si@open.spotify.com",
|
||||
"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 definePlugin, { OptionType } from "@utils/types";
|
||||
import { maybePromptToUpdate } from "@utils/updater";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { FluxDispatcher, NavigationRouter } from "@webpack/common";
|
||||
import type { ReactElement } from "react";
|
||||
|
||||
const CrashHandlerLogger = new Logger("CrashHandler");
|
||||
const ModalStack = findByPropsLazy("pushLazy", "popAll");
|
||||
|
||||
const settings = definePluginSettings({
|
||||
attemptToPreventCrashes: {
|
||||
@ -51,8 +53,6 @@ export default definePlugin({
|
||||
authors: [Devs.Nuckyz],
|
||||
enabledByDefault: true,
|
||||
|
||||
popAllModals: undefined as (() => void) | undefined,
|
||||
|
||||
settings,
|
||||
|
||||
patches: [
|
||||
@ -62,13 +62,6 @@ export default definePlugin({
|
||||
match: /(?=this\.setState\()/,
|
||||
replace: "$self.handleCrash(this)||"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: 'dispatch({type:"MODAL_POP_ALL"})',
|
||||
replacement: {
|
||||
match: /"MODAL_POP_ALL".+?};(?<=(\i)=function.+?)/,
|
||||
replace: (m, popAll) => `${m}$self.popAllModals=${popAll};`
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@ -128,7 +121,7 @@ export default definePlugin({
|
||||
CrashHandlerLogger.debug("Failed to close open context menu.", err);
|
||||
}
|
||||
try {
|
||||
this.popAllModals?.();
|
||||
ModalStack?.popAll();
|
||||
} catch (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/>.
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { definePluginSettings, Settings } from "@api/Settings";
|
||||
import { Link } from "@components/Link";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { isTruthy } from "@utils/guards";
|
||||
@ -86,8 +86,7 @@ const settings = definePluginSettings({
|
||||
appID: {
|
||||
type: OptionType.STRING,
|
||||
description: "Application ID (required)",
|
||||
restartNeeded: true,
|
||||
onChange: setRpc,
|
||||
onChange: onChange,
|
||||
isValid: (value: string) => {
|
||||
if (!value) return "Application ID is required.";
|
||||
if (value && !/^\d+$/.test(value)) return "Application ID must be a number.";
|
||||
@ -97,8 +96,7 @@ const settings = definePluginSettings({
|
||||
appName: {
|
||||
type: OptionType.STRING,
|
||||
description: "Application name (required)",
|
||||
restartNeeded: true,
|
||||
onChange: setRpc,
|
||||
onChange: onChange,
|
||||
isValid: (value: string) => {
|
||||
if (!value) return "Application name is required.";
|
||||
if (value.length > 128) return "Application name must be not longer than 128 characters.";
|
||||
@ -108,8 +106,7 @@ const settings = definePluginSettings({
|
||||
details: {
|
||||
type: OptionType.STRING,
|
||||
description: "Details (line 1)",
|
||||
restartNeeded: true,
|
||||
onChange: setRpc,
|
||||
onChange: onChange,
|
||||
isValid: (value: string) => {
|
||||
if (value && value.length > 128) return "Details (line 1) must be not longer than 128 characters.";
|
||||
return true;
|
||||
@ -118,8 +115,7 @@ const settings = definePluginSettings({
|
||||
state: {
|
||||
type: OptionType.STRING,
|
||||
description: "State (line 2)",
|
||||
restartNeeded: true,
|
||||
onChange: setRpc,
|
||||
onChange: onChange,
|
||||
isValid: (value: string) => {
|
||||
if (value && value.length > 128) return "State (line 2) must be not longer than 128 characters.";
|
||||
return true;
|
||||
@ -128,8 +124,7 @@ const settings = definePluginSettings({
|
||||
type: {
|
||||
type: OptionType.SELECT,
|
||||
description: "Activity type",
|
||||
restartNeeded: true,
|
||||
onChange: setRpc,
|
||||
onChange: onChange,
|
||||
options: [
|
||||
{
|
||||
label: "Playing",
|
||||
@ -157,16 +152,14 @@ const settings = definePluginSettings({
|
||||
streamLink: {
|
||||
type: OptionType.STRING,
|
||||
description: "Twitch.tv or Youtube.com link (only for Streaming activity type)",
|
||||
restartNeeded: true,
|
||||
onChange: setRpc,
|
||||
isDisabled: isStreamLinkDisabled,
|
||||
onChange: onChange,
|
||||
disabled: isStreamLinkDisabled,
|
||||
isValid: isStreamLinkValid
|
||||
},
|
||||
timestampMode: {
|
||||
type: OptionType.SELECT,
|
||||
description: "Timestamp mode",
|
||||
restartNeeded: true,
|
||||
onChange: setRpc,
|
||||
onChange: onChange,
|
||||
options: [
|
||||
{
|
||||
label: "None",
|
||||
@ -190,9 +183,8 @@ const settings = definePluginSettings({
|
||||
startTime: {
|
||||
type: OptionType.NUMBER,
|
||||
description: "Start timestamp (only for custom timestamp mode)",
|
||||
restartNeeded: true,
|
||||
onChange: setRpc,
|
||||
isDisabled: isTimestampDisabled,
|
||||
onChange: onChange,
|
||||
disabled: isTimestampDisabled,
|
||||
isValid: (value: number) => {
|
||||
if (value && value < 0) return "Start timestamp must be greater than 0.";
|
||||
return true;
|
||||
@ -201,9 +193,8 @@ const settings = definePluginSettings({
|
||||
endTime: {
|
||||
type: OptionType.NUMBER,
|
||||
description: "End timestamp (only for custom timestamp mode)",
|
||||
restartNeeded: true,
|
||||
onChange: setRpc,
|
||||
isDisabled: isTimestampDisabled,
|
||||
onChange: onChange,
|
||||
disabled: isTimestampDisabled,
|
||||
isValid: (value: number) => {
|
||||
if (value && value < 0) return "End timestamp must be greater than 0.";
|
||||
return true;
|
||||
@ -211,16 +202,14 @@ const settings = definePluginSettings({
|
||||
},
|
||||
imageBig: {
|
||||
type: OptionType.STRING,
|
||||
description: "Big image key",
|
||||
restartNeeded: true,
|
||||
onChange: setRpc,
|
||||
description: "Big image key/link",
|
||||
onChange: onChange,
|
||||
isValid: isImageKeyValid
|
||||
},
|
||||
imageBigTooltip: {
|
||||
type: OptionType.STRING,
|
||||
description: "Big image tooltip",
|
||||
restartNeeded: true,
|
||||
onChange: setRpc,
|
||||
onChange: onChange,
|
||||
isValid: (value: string) => {
|
||||
if (value && value.length > 128) return "Big image tooltip must be not longer than 128 characters.";
|
||||
return true;
|
||||
@ -228,16 +217,14 @@ const settings = definePluginSettings({
|
||||
},
|
||||
imageSmall: {
|
||||
type: OptionType.STRING,
|
||||
description: "Small image key",
|
||||
restartNeeded: true,
|
||||
onChange: setRpc,
|
||||
description: "Small image key/link",
|
||||
onChange: onChange,
|
||||
isValid: isImageKeyValid
|
||||
},
|
||||
imageSmallTooltip: {
|
||||
type: OptionType.STRING,
|
||||
description: "Small image tooltip",
|
||||
restartNeeded: true,
|
||||
onChange: setRpc,
|
||||
onChange: onChange,
|
||||
isValid: (value: string) => {
|
||||
if (value && value.length > 128) return "Small image tooltip must be not longer than 128 characters.";
|
||||
return true;
|
||||
@ -246,8 +233,7 @@ const settings = definePluginSettings({
|
||||
buttonOneText: {
|
||||
type: OptionType.STRING,
|
||||
description: "Button 1 text",
|
||||
restartNeeded: true,
|
||||
onChange: setRpc,
|
||||
onChange: onChange,
|
||||
isValid: (value: string) => {
|
||||
if (value && value.length > 31) return "Button 1 text must be not longer than 31 characters.";
|
||||
return true;
|
||||
@ -256,14 +242,12 @@ const settings = definePluginSettings({
|
||||
buttonOneURL: {
|
||||
type: OptionType.STRING,
|
||||
description: "Button 1 URL",
|
||||
restartNeeded: true,
|
||||
onChange: setRpc
|
||||
onChange: onChange
|
||||
},
|
||||
buttonTwoText: {
|
||||
type: OptionType.STRING,
|
||||
description: "Button 2 text",
|
||||
restartNeeded: true,
|
||||
onChange: setRpc,
|
||||
onChange: onChange,
|
||||
isValid: (value: string) => {
|
||||
if (value && value.length > 31) return "Button 2 text must be not longer than 31 characters.";
|
||||
return true;
|
||||
@ -272,26 +256,29 @@ const settings = definePluginSettings({
|
||||
buttonTwoURL: {
|
||||
type: OptionType.STRING,
|
||||
description: "Button 2 URL",
|
||||
restartNeeded: true,
|
||||
onChange: setRpc
|
||||
onChange: onChange
|
||||
}
|
||||
});
|
||||
|
||||
function isStreamLinkDisabled(): boolean {
|
||||
function onChange() {
|
||||
setRpc(true);
|
||||
if (Settings.plugins.CustomRPC.enabled) setRpc();
|
||||
}
|
||||
|
||||
function isStreamLinkDisabled() {
|
||||
return settings.store.type !== ActivityType.STREAMING;
|
||||
}
|
||||
|
||||
function isStreamLinkValid(): boolean | 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.";
|
||||
function isStreamLinkValid(value: string) {
|
||||
if (!isStreamLinkDisabled() && !/https?:\/\/(www\.)?(twitch\.tv|youtube\.com)\/\w+/.test(value)) return "Streaming link must be a valid URL.";
|
||||
return true;
|
||||
}
|
||||
|
||||
function isTimestampDisabled(): boolean {
|
||||
function isTimestampDisabled() {
|
||||
return settings.store.timestampMode !== TimestampMode.CUSTOM;
|
||||
}
|
||||
|
||||
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?:\/\/(?!media\.)?tenor\.com\//.test(value)) return "Tenor link must be a direct link to the image. (e.g. https://media.tenor.com/...)";
|
||||
return true;
|
||||
@ -342,13 +329,10 @@ async function createActivity(): Promise<Activity | undefined> {
|
||||
};
|
||||
break;
|
||||
case TimestampMode.CUSTOM:
|
||||
if (startTime) {
|
||||
activity.timestamps = {
|
||||
start: startTime,
|
||||
};
|
||||
if (endTime) {
|
||||
activity.timestamps.end = endTime;
|
||||
}
|
||||
if (startTime || endTime) {
|
||||
activity.timestamps = {};
|
||||
if (startTime) activity.timestamps.start = startTime;
|
||||
if (endTime) activity.timestamps.end = endTime;
|
||||
}
|
||||
break;
|
||||
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 { definePluginSettings, Settings } from "@api/Settings";
|
||||
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 { proxyLazy } from "@utils/lazy";
|
||||
import { Logger } from "@utils/Logger";
|
||||
@ -27,6 +27,7 @@ import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByCodeLazy, findByPropsLazy, findLazy, findStoreLazy } from "@webpack";
|
||||
import { ChannelStore, EmojiStore, FluxDispatcher, Parser, PermissionStore, UserStore } from "@webpack/common";
|
||||
import type { Message } from "discord-types/general";
|
||||
import { applyPalette, GIFEncoder, quantize } from "gifenc";
|
||||
import type { ReactElement, ReactNode } from "react";
|
||||
|
||||
const DRAFT_TYPE = 0;
|
||||
@ -102,6 +103,11 @@ interface StickerPack {
|
||||
stickers: Sticker[];
|
||||
}
|
||||
|
||||
const enum FakeNoticeType {
|
||||
Sticker,
|
||||
Emoji
|
||||
}
|
||||
|
||||
const fakeNitroEmojiRegex = /\/emojis\/(\d+?)\.(png|webp|gif)/;
|
||||
const fakeNitroStickerRegex = /\/stickers\/(\d+?)\./;
|
||||
const fakeNitroGifStickerRegex = /\/attachments\/\d+?\/\d+?\/(\d+?)\.gif/;
|
||||
@ -212,15 +218,15 @@ export default definePlugin({
|
||||
}
|
||||
},
|
||||
{
|
||||
find: "canStreamHighQuality:function",
|
||||
find: "canUseHighVideoUploadQuality:function",
|
||||
predicate: () => settings.store.enableStreamQualityBypass,
|
||||
replacement: [
|
||||
"canUseHighVideoUploadQuality",
|
||||
"canStreamHighQuality",
|
||||
"canStreamMidQuality"
|
||||
// TODO: Remove the last two when they get removed from stable
|
||||
"(?:canStreamQuality|canStreamHighQuality|canStreamMidQuality)",
|
||||
].map(func => {
|
||||
return {
|
||||
match: new RegExp(`${func}:function\\(\\i\\){`),
|
||||
match: new RegExp(`${func}:function\\(\\i(?:,\\i)?\\){`, "g"),
|
||||
replace: "$&return true;"
|
||||
};
|
||||
})
|
||||
@ -305,16 +311,24 @@ export default definePlugin({
|
||||
},
|
||||
{
|
||||
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,
|
||||
replacement: {
|
||||
match: /((\i)=\i\.node,\i=\i\.expressionSourceGuild)(.+?return )(.{0,450}Messages\.EMOJI_POPOUT_PREMIUM_JOINED_GUILD_DESCRIPTION.+?}\))/,
|
||||
replace: (_, rest1, node, rest2, reactNode) => `${rest1},fakeNitroNode=${node}${rest2}$self.addFakeNotice("EMOJI",${reactNode},fakeNitroNode.fake)`
|
||||
match: /isDiscoverable:\i,shouldHideRoleSubscriptionCTA:\i,(?<=(\i)=\i\.node.+?)/,
|
||||
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 => `$self.addFakeNotice(${FakeNoticeType.Emoji},${reactNode},!!arguments[0]?.fakeNitroNode?.fake)`
|
||||
}
|
||||
}
|
||||
],
|
||||
@ -610,18 +624,18 @@ export default definePlugin({
|
||||
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;
|
||||
|
||||
node = Array.isArray(node) ? node : [node];
|
||||
|
||||
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.");
|
||||
|
||||
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.");
|
||||
|
||||
return node;
|
||||
@ -650,15 +664,11 @@ export default definePlugin({
|
||||
},
|
||||
|
||||
async sendAnimatedSticker(stickerLink: string, stickerId: string, channelId: string) {
|
||||
const [{ parseURL }, {
|
||||
GIFEncoder,
|
||||
quantize,
|
||||
applyPalette
|
||||
}] = await Promise.all([importApngJs(), getGifEncoder()]);
|
||||
const { parseURL } = importApngJs();
|
||||
|
||||
const { frames, width, height } = await parseURL(stickerLink);
|
||||
|
||||
const gif = new GIFEncoder();
|
||||
const gif = GIFEncoder();
|
||||
const resolution = Settings.plugins.FakeNitro.stickerSize;
|
||||
|
||||
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({
|
||||
searchOption: {
|
||||
@ -87,7 +87,7 @@ export const settings = definePluginSettings({
|
||||
export default definePlugin({
|
||||
name: "FavoriteGifSearch",
|
||||
authors: [Devs.Aria],
|
||||
description: "Adds a search bar for favorite gifs",
|
||||
description: "Adds a search bar to favorite gifs.",
|
||||
|
||||
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
|
||||
}
|
||||
})
|
||||
});
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
* 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
|
||||
@ -16,7 +16,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { ApplicationCommandInputType, sendBotMessage } from "@api/Commands";
|
||||
import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
@ -35,30 +35,47 @@ export default definePlugin({
|
||||
name: "create friend invite",
|
||||
description: "Generates a friend invite link.",
|
||||
inputType: ApplicationCommandInputType.BOT,
|
||||
execute: async (_, ctx) => {
|
||||
if (!UserStore.getCurrentUser().phone)
|
||||
options: [{
|
||||
name: "Uses",
|
||||
description: "How many uses?",
|
||||
choices: [
|
||||
{ label: "1", name: "1", value: "1" },
|
||||
{ label: "5", name: "5", value: "5" }
|
||||
],
|
||||
required: false,
|
||||
type: ApplicationCommandOptionType.INTEGER
|
||||
}],
|
||||
|
||||
execute: async (args, ctx) => {
|
||||
const uses = findOption<number>(args, "Uses", 5);
|
||||
|
||||
if (uses === 1 && !UserStore.getCurrentUser().phone)
|
||||
return sendBotMessage(ctx.channel.id, {
|
||||
content: "You need to have a phone number connected to your account to create a friend invite!"
|
||||
content: "You need to have a phone number connected to your account to create a friend invite with 1 use!"
|
||||
});
|
||||
|
||||
const random = uuid.v4();
|
||||
const invite = await RestAPI.post({
|
||||
url: "/friend-finder/find-friends",
|
||||
body: {
|
||||
modified_contacts: {
|
||||
[random]: [1, "", ""]
|
||||
},
|
||||
phone_contact_methods_count: 1
|
||||
}
|
||||
}).then(res =>
|
||||
FriendInvites.createFriendInvite({
|
||||
code: res.body.invite_suggestions[0][3],
|
||||
let invite: any;
|
||||
if (uses === 1) {
|
||||
const random = uuid.v4();
|
||||
const { body: { invite_suggestions } } = await RestAPI.post({
|
||||
url: "/friend-finder/find-friends",
|
||||
body: {
|
||||
modified_contacts: {
|
||||
[random]: [1, "", ""]
|
||||
},
|
||||
phone_contact_methods_count: 1
|
||||
}
|
||||
});
|
||||
invite = await FriendInvites.createFriendInvite({
|
||||
code: invite_suggestions[0][3],
|
||||
recipient_phone_number_or_email: random,
|
||||
contact_visibility: 1,
|
||||
filter_visibilities: [],
|
||||
filtered_invite_suggestions_index: 1
|
||||
})
|
||||
);
|
||||
});
|
||||
} else {
|
||||
invite = await FriendInvites.createFriendInvite();
|
||||
}
|
||||
|
||||
sendBotMessage(ctx.channel.id, {
|
||||
content: `
|
||||
@ -67,7 +84,7 @@ export default definePlugin({
|
||||
Max uses: \`${invite.max_uses}\`
|
||||
`.trim().replace(/\s+/g, " ")
|
||||
});
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "view friend invites",
|
||||
@ -95,7 +112,7 @@ export default definePlugin({
|
||||
execute: async (_, ctx) => {
|
||||
await FriendInvites.revokeFriendInvites();
|
||||
|
||||
return void sendBotMessage(ctx.channel.id, {
|
||||
sendBotMessage(ctx.channel.id, {
|
||||
content: "All friend invites have been revoked."
|
||||
});
|
||||
},
|
||||
|
@ -32,12 +32,18 @@ function makeIcon(showCurrentGame?: boolean) {
|
||||
return function () {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 96 960 960"
|
||||
width="20"
|
||||
height="20"
|
||||
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" />
|
||||
{!showCurrentGame && <line x1="920" y1="280" x2="40" y2="880" stroke="var(--status-danger)" stroke-width="80" />}
|
||||
<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 && <>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
@ -60,7 +66,7 @@ function GameActivityToggleButton() {
|
||||
export default definePlugin({
|
||||
name: "GameActivityToggle",
|
||||
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"],
|
||||
|
||||
patches: [
|
||||
|
@ -49,7 +49,7 @@ export default definePlugin({
|
||||
await this.buildCss();
|
||||
|
||||
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);
|
||||
|
||||
@ -72,7 +72,7 @@ export default definePlugin({
|
||||
async buildCss() {
|
||||
const elements = [...hiddenMessages].map(id => `#message-accessories-${id}`).join(",");
|
||||
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 */
|
||||
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();
|
||||
}}
|
||||
/>
|
||||
<Menu.MenuCheckboxItem
|
||||
id="vc-nearest-neighbour"
|
||||
label="Nearset Neighbour"
|
||||
checked={settings.store.nearestNeighbour}
|
||||
action={() => {
|
||||
settings.store.nearestNeighbour = !settings.store.nearestNeighbour;
|
||||
ContextMenu.close();
|
||||
}}
|
||||
/>
|
||||
<Menu.MenuControlItem
|
||||
id="vc-zoom"
|
||||
label="Zoom"
|
||||
|
@ -28,7 +28,7 @@ import { Button, Forms, React, TextInput } from "@webpack/common";
|
||||
import { decrypt } from "../index";
|
||||
|
||||
export function DecModal(props: any) {
|
||||
const secret: string = props?.message?.content;
|
||||
const encryptedMessage: string = props?.message?.content;
|
||||
const [password, setPassword] = React.useState("password");
|
||||
|
||||
return (
|
||||
@ -38,9 +38,9 @@ export function DecModal(props: any) {
|
||||
</ModalHeader>
|
||||
|
||||
<ModalContent>
|
||||
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Secret</Forms.FormTitle>
|
||||
<TextInput defaultValue={secret} disabled={true}></TextInput>
|
||||
<Forms.FormTitle tag="h5">Password</Forms.FormTitle>
|
||||
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Message with Encryption</Forms.FormTitle>
|
||||
<TextInput defaultValue={encryptedMessage} disabled={true}></TextInput>
|
||||
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Password</Forms.FormTitle>
|
||||
<TextInput
|
||||
style={{ marginBottom: "20px" }}
|
||||
onChange={setPassword}
|
||||
@ -51,7 +51,7 @@ export function DecModal(props: any) {
|
||||
<Button
|
||||
color={Button.Colors.GREEN}
|
||||
onClick={() => {
|
||||
const toSend = decrypt(secret, password, true);
|
||||
const toSend = decrypt(encryptedMessage, password, true);
|
||||
if (!toSend || !props?.message) return;
|
||||
// @ts-expect-error
|
||||
Vencord.Plugins.plugins.InvisibleChat.buildEmbed(props?.message, toSend);
|
@ -225,8 +225,8 @@ export function encrypt(secret: string, password: string, cover: string): string
|
||||
return steggo.hide(secret + "\u200b", password, cover);
|
||||
}
|
||||
|
||||
export function decrypt(secret: string, password: string, removeIndicator: boolean): string {
|
||||
const decrypted = steggo.reveal(secret, password);
|
||||
export function decrypt(encrypted: string, password: string, removeIndicator: boolean): string {
|
||||
const decrypted = steggo.reveal(encrypted, password);
|
||||
return removeIndicator ? decrypted.replace("\u200b", "") : decrypted;
|
||||
}
|
||||
|
@ -72,6 +72,14 @@ const enum ActivityFlag {
|
||||
INSTANCE = 1 << 0,
|
||||
}
|
||||
|
||||
const enum NameFormat {
|
||||
StatusName = "status-name",
|
||||
ArtistFirst = "artist-first",
|
||||
SongFirst = "song-first",
|
||||
ArtistOnly = "artist",
|
||||
SongOnly = "song"
|
||||
}
|
||||
|
||||
const applicationId = "1108588077900898414";
|
||||
const placeholderId = "2a96cbd8b46e442fc41c2b86b821562f";
|
||||
|
||||
@ -117,10 +125,37 @@ const settings = definePluginSettings({
|
||||
default: true,
|
||||
},
|
||||
statusName: {
|
||||
description: "text shown in status",
|
||||
description: "custom status text",
|
||||
type: OptionType.STRING,
|
||||
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: {
|
||||
description: 'show "Listening to" status instead of "Playing"',
|
||||
type: OptionType.BOOLEAN,
|
||||
@ -140,13 +175,13 @@ const settings = definePluginSettings({
|
||||
value: "placeholder"
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "LastFMRichPresence",
|
||||
description: "Little plugin for Last.fm rich presence",
|
||||
authors: [Devs.dzshn, Devs.RuiNtD],
|
||||
authors: [Devs.dzshn, Devs.RuiNtD, Devs.blahajZip, Devs.archeruwu],
|
||||
|
||||
settingsAboutComponent: () => (
|
||||
<>
|
||||
@ -267,9 +302,24 @@ export default definePlugin({
|
||||
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 {
|
||||
application_id: applicationId,
|
||||
name: settings.store.statusName,
|
||||
name: statusName,
|
||||
|
||||
details: trackData.name,
|
||||
state: trackData.artist,
|
||||
|
@ -56,7 +56,8 @@ const quotes = [
|
||||
"hd{b${",
|
||||
"<;vqkijbq33271:56<3799?24944:",
|
||||
"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({
|
||||
|
@ -30,6 +30,9 @@ const ChannelMemberStore = findStoreLazy("ChannelMemberStore") as FluxStore & {
|
||||
getProps(guildId: string, channelId: string): { groups: { count: number; id: string; }[]; };
|
||||
};
|
||||
|
||||
const sharedIntlNumberFormat = new Intl.NumberFormat();
|
||||
const numberFormat = (value: number) => sharedIntlNumberFormat.format(value);
|
||||
|
||||
function MemberCount() {
|
||||
const { id: channelId, guild_id: guildId } = useStateFromStores([SelectedChannelStore], () => getCurrentChannel());
|
||||
const { groups } = useStateFromStores(
|
||||
@ -57,7 +60,7 @@ function MemberCount() {
|
||||
alignContent: "center",
|
||||
gap: 0
|
||||
}}>
|
||||
<Tooltip text={`${online} Online in this Channel`} position="bottom">
|
||||
<Tooltip text={`${numberFormat(online)} online in this channel`} position="bottom">
|
||||
{props => (
|
||||
<div {...props}>
|
||||
<span
|
||||
@ -70,11 +73,11 @@ function MemberCount() {
|
||||
marginRight: "0.5em"
|
||||
}}
|
||||
/>
|
||||
<span style={{ color: "var(--green-360)" }}>{online}</span>
|
||||
<span style={{ color: "var(--green-360)" }}>{numberFormat(online)}</span>
|
||||
</div>
|
||||
)}
|
||||
</Tooltip>
|
||||
<Tooltip text={`${total} Total Server Members`} position="bottom">
|
||||
<Tooltip text={`${numberFormat(total)} total server members`} position="bottom">
|
||||
{props => (
|
||||
<div {...props}>
|
||||
<span
|
||||
@ -88,7 +91,7 @@ function MemberCount() {
|
||||
marginLeft: "1em"
|
||||
}}
|
||||
/>
|
||||
<span style={{ color: "var(--primary-400)" }}>{total}</span>
|
||||
<span style={{ color: "var(--primary-400)" }}>{numberFormat(total)}</span>
|
||||
</div>
|
||||
)}
|
||||
</Tooltip>
|
||||
|
@ -18,6 +18,7 @@
|
||||
|
||||
import { addAccessory } from "@api/MessageAccessories";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { getSettingStoreLazy } from "@api/SettingsStore";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants.js";
|
||||
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 {
|
||||
const { message, channel, guildID } = props;
|
||||
|
||||
const isDM = guildID === "@me";
|
||||
const images = getImages(message);
|
||||
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>
|
||||
</Text>
|
||||
}
|
||||
compact={false}
|
||||
compact={compactModeEnabled.getSetting()}
|
||||
content={
|
||||
<>
|
||||
{message.content || message.attachments.length <= images.length
|
||||
@ -363,7 +365,7 @@ export default definePlugin({
|
||||
name: "MessageLinkEmbeds",
|
||||
description: "Adds a preview to messages that link another message",
|
||||
authors: [Devs.TheSun, Devs.Ven, Devs.RyanCaoDev],
|
||||
dependencies: ["MessageAccessoriesAPI"],
|
||||
dependencies: ["MessageAccessoriesAPI", "SettingsStoreAPI"],
|
||||
patches: [
|
||||
{
|
||||
find: ".embedCard",
|
||||
|
@ -169,21 +169,14 @@ export default definePlugin({
|
||||
try {
|
||||
if (cache == null || (!isBulk && !cache.has(data.id))) return cache;
|
||||
|
||||
const { ignoreBots, ignoreSelf, ignoreUsers, ignoreChannels, ignoreGuilds } = Settings.plugins.MessageLogger;
|
||||
const myId = UserStore.getCurrentUser().id;
|
||||
|
||||
function mutate(id: string) {
|
||||
const mutate = (id: string) => {
|
||||
const msg = cache.get(id);
|
||||
if (!msg) return;
|
||||
|
||||
const EPHEMERAL = 64;
|
||||
const shouldIgnore = data.mlDeleted ||
|
||||
(msg.flags & EPHEMERAL) === EPHEMERAL ||
|
||||
ignoreBots && msg.author?.bot ||
|
||||
ignoreSelf && msg.author?.id === myId ||
|
||||
ignoreUsers.includes(msg.author?.id) ||
|
||||
ignoreChannels.includes(msg.channel_id) ||
|
||||
ignoreGuilds.includes(ChannelStore.getChannel(msg.channel_id)?.guild_id);
|
||||
this.shouldIgnore(msg);
|
||||
|
||||
if (shouldIgnore) {
|
||||
cache = cache.remove(id);
|
||||
@ -192,7 +185,7 @@ export default definePlugin({
|
||||
.set("deleted", true)
|
||||
.set("attachments", m.attachments.map(a => (a.deleted = true, a))));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (isBulk) {
|
||||
data.ids.forEach(mutate);
|
||||
@ -205,6 +198,18 @@ export default definePlugin({
|
||||
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
|
||||
patches: [
|
||||
{
|
||||
@ -237,7 +242,7 @@ export default definePlugin({
|
||||
match: /(MESSAGE_UPDATE:function\((\w)\).+?)\.update\((\w)/,
|
||||
replace: "$1" +
|
||||
".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 ?" +
|
||||
" m.set('editHistory',[...(m.editHistory || []), $self.makeEdit($2.message, m)]) :" +
|
||||
" m" +
|
||||
@ -321,14 +326,14 @@ export default definePlugin({
|
||||
{
|
||||
// Attachment renderer
|
||||
// Module 96063
|
||||
find: "[\"className\",\"attachment\",\"inlineMedia\"",
|
||||
find: "().removeAttachmentHoverButton",
|
||||
replacement: [
|
||||
{
|
||||
match: /((\w)\.className,\w=\2\.attachment),/,
|
||||
replace: "$1,deleted=$2.attachment?.deleted,"
|
||||
},
|
||||
{
|
||||
match: /\["className","attachment","inlineMedia".+?className:/,
|
||||
match: /\["className","attachment".+?className:/,
|
||||
replace: "$& (deleted ? 'messagelogger-deleted-attachment ' : '') +"
|
||||
}
|
||||
]
|
||||
|
@ -185,7 +185,7 @@ const settings = definePluginSettings({
|
||||
export default definePlugin({
|
||||
name: "MoreUserTags",
|
||||
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,
|
||||
patches: [
|
||||
// add tags to the tag list
|
||||
@ -232,9 +232,8 @@ export default definePlugin({
|
||||
{
|
||||
find: ".renderBot=function(){",
|
||||
replacement: {
|
||||
match: /this.props.user;return null!=(\i)&&.{0,10}\?(.{0,50})\.botTag/,
|
||||
replace: "this.props.user;var type=$self.getTag({...this.props,origType:$1.bot?0:null,location:'not-chat'});\
|
||||
return type!==null?$2.botTag,type"
|
||||
match: /\.BOT;return null!=(\i)&&.{0,10}\?(.{0,50})\.botTag,type:\i/,
|
||||
replace: ".BOT;var type=$self.getTag({...this.props,origType:$1.bot?0:null,location:'not-chat'});return type!==null?$2.botTag,type"
|
||||
}
|
||||
},
|
||||
// pass channel id down props to be used in profiles
|
||||
|
@ -54,6 +54,8 @@ interface IVoiceChannelEffectSendEvent {
|
||||
const MOYAI = "🗿";
|
||||
const MOYAI_URL =
|
||||
"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({
|
||||
volume: {
|
||||
@ -63,6 +65,14 @@ const settings = definePluginSettings({
|
||||
default: 0.5,
|
||||
stickToMarkers: false
|
||||
},
|
||||
quality: {
|
||||
description: "Quality of the 🗿🗿🗿",
|
||||
type: OptionType.SELECT,
|
||||
options: [
|
||||
{ label: "Normal", value: "Normal", default: true },
|
||||
{ label: "HD", value: "HD" }
|
||||
],
|
||||
},
|
||||
triggerWhenUnfocused: {
|
||||
description: "Trigger the 🗿 even when the window is unfocused",
|
||||
type: OptionType.BOOLEAN,
|
||||
@ -157,7 +167,11 @@ function getMoyaiCount(message: string) {
|
||||
function boom() {
|
||||
if (!settings.store.triggerWhenUnfocused && !document.hasFocus()) return;
|
||||
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.play();
|
||||
}
|
||||
|
@ -55,8 +55,8 @@ export default definePlugin({
|
||||
{
|
||||
find: '"MaskedLinkStore"',
|
||||
replacement: {
|
||||
match: /return ((\i)\.apply\(this,arguments\))(?=\}function \i.{0,200}\.trusted)/,
|
||||
replace: "return $self.handleLink(...arguments).then(handled => handled || $1)"
|
||||
match: /return ((\i)\.apply\(this,arguments\))(?=\}function \i.{0,250}\.trusted)/,
|
||||
replace: "return $self.handleLink(...arguments).then(handled => handled||$1)"
|
||||
}
|
||||
},
|
||||
// Make Spotify profile activity links open in app on web
|
||||
|
@ -178,12 +178,12 @@ export default definePlugin({
|
||||
start() {
|
||||
addContextMenuPatch("user-context", this.userContextMenuPatch);
|
||||
addContextMenuPatch("channel-context", this.channelContextMenuPatch);
|
||||
addContextMenuPatch("guild-context", this.guildContextMenuPatch);
|
||||
addContextMenuPatch(["guild-context", "guild-header-popout"], this.guildContextMenuPatch);
|
||||
},
|
||||
|
||||
stop() {
|
||||
removeContextMenuPatch("user-context", this.userContextMenuPatch);
|
||||
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 { Devs } from "@utils/constants";
|
||||
import { getGifEncoder } from "@utils/dependencies";
|
||||
import { makeLazy } from "@utils/lazy";
|
||||
import definePlugin from "@utils/types";
|
||||
import { findByCodeLazy, findByPropsLazy } from "@webpack";
|
||||
import { applyPalette, GIFEncoder, quantize } from "gifenc";
|
||||
|
||||
const DRAFT_TYPE = 0;
|
||||
const DEFAULT_DELAY = 20;
|
||||
@ -124,7 +124,6 @@ export default definePlugin({
|
||||
}
|
||||
],
|
||||
execute: async (opts, cmdCtx) => {
|
||||
const { GIFEncoder, quantize, applyPalette } = await getGifEncoder();
|
||||
const frames = await getFrames();
|
||||
|
||||
const noServerPfp = findOption(opts, "no-server-pfp", false);
|
||||
@ -143,7 +142,7 @@ export default definePlugin({
|
||||
const delay = findOption(opts, "delay", DEFAULT_DELAY);
|
||||
const resolution = findOption(opts, "resolution", DEFAULT_RESOLUTION);
|
||||
|
||||
const gif = new GIFEncoder();
|
||||
const gif = GIFEncoder();
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
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)))
|
||||
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)
|
@ -16,38 +16,96 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { sendBotMessage } from "@api/Commands";
|
||||
import { generateId, sendBotMessage } from "@api/Commands";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import { Button, ButtonLooks, ButtonWrapperClasses, DraftStore, DraftType, SelectedChannelStore, Tooltip, UserStore } from "@webpack/common";
|
||||
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);
|
||||
|
||||
export function PreviewButton(chatBoxProps: Props) {
|
||||
if (chatBoxProps.type.analyticsName !== "normal") return null;
|
||||
const channelId = SelectedChannelStore.getChannelId();
|
||||
const draft = getDraft(channelId);
|
||||
|
||||
if (!draft) return null;
|
||||
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={() =>
|
||||
onClick={async () =>
|
||||
sendBotMessage(
|
||||
channelId,
|
||||
{
|
||||
content: getDraft(channelId),
|
||||
author: UserStore.getCurrentUser()
|
||||
author: UserStore.getCurrentUser(),
|
||||
attachments: hasAttachments ? await getAttachments(channelId) : undefined,
|
||||
}
|
||||
)}
|
||||
size=""
|
||||
@ -67,7 +125,7 @@ export function PreviewButton(chatBoxProps: Props) {
|
||||
|
||||
export default definePlugin({
|
||||
name: "PreviewMessage",
|
||||
description: "Lets you preview your message before sending it",
|
||||
description: "Lets you preview your message before sending it.",
|
||||
authors: [Devs.Aria],
|
||||
patches: [
|
||||
{
|
@ -21,14 +21,11 @@ import { VENCORD_USER_AGENT } from "@utils/constants";
|
||||
import { debounce } from "@utils/debounce";
|
||||
import { getCurrentChannel } from "@utils/discord";
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import { findStoreLazy } from "@webpack";
|
||||
import { UserStore } from "@webpack/common";
|
||||
import { UserProfileStore, UserStore } from "@webpack/common";
|
||||
|
||||
import { settings } from "./settings";
|
||||
import { PronounCode, PronounMapping, PronounsResponse } from "./types";
|
||||
|
||||
const UserProfileStore = findStoreLazy("UserProfileStore");
|
||||
|
||||
type PronounsWithSource = [string | null, string];
|
||||
const EmptyPronouns: PronounsWithSource = [null, ""];
|
||||
|
||||
|
@ -28,8 +28,8 @@ export default function ReviewBadge(badge: Badge) {
|
||||
{({ onMouseEnter, onMouseLeave }) => (
|
||||
<img
|
||||
className={cl("badge")}
|
||||
width="24px"
|
||||
height="24px"
|
||||
width="22px"
|
||||
height="22px"
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
src={badge.icon}
|
||||
|
@ -20,7 +20,7 @@ import { openUserProfile } from "@utils/discord";
|
||||
import { classes } from "@utils/misc";
|
||||
import { LazyComponent } from "@utils/react";
|
||||
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 { deleteReview, reportReview } from "../reviewDbApi";
|
||||
@ -30,12 +30,12 @@ import { DeleteButton, ReportButton } from "./MessageButton";
|
||||
import ReviewBadge from "./ReviewBadge";
|
||||
|
||||
export default LazyComponent(() => {
|
||||
// this is terrible, blame mantika
|
||||
// this is terrible, blame ven
|
||||
const p = filters.byProps;
|
||||
const [
|
||||
{ cozyMessage, buttons, message, buttonsInner, groupStart },
|
||||
{ container, isHeader },
|
||||
{ avatar, clickable, username, messageContent, wrapper, cozy },
|
||||
{ avatar, clickable, username, wrapper, cozy },
|
||||
buttonClasses,
|
||||
botTag
|
||||
] = findBulk(
|
||||
@ -124,12 +124,10 @@ export default LazyComponent(() => {
|
||||
</Timestamp>)
|
||||
}
|
||||
|
||||
<p
|
||||
className={classes(messageContent)}
|
||||
style={{ fontSize: 15, marginTop: 4, color: "var(--text-normal)" }}
|
||||
>
|
||||
{review.comment}
|
||||
</p>
|
||||
<div className={cl("review-comment")}>
|
||||
{Parser.parseGuildEventDescription(review.comment)}
|
||||
</div>
|
||||
|
||||
{review.id !== 0 && (
|
||||
<div className={classes(container, isHeader, buttons)} style={{
|
||||
padding: "0px",
|
||||
|
@ -16,11 +16,9 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { classes } from "@utils/misc";
|
||||
import { useAwaiter, useForceUpdater } from "@utils/react";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { Forms, React, RelationshipStore, UserStore } from "@webpack/common";
|
||||
import type { KeyboardEvent } from "react";
|
||||
import { LazyComponent, useAwaiter, useForceUpdater } from "@utils/react";
|
||||
import { find, findByPropsLazy } from "@webpack";
|
||||
import { Forms, React, RelationshipStore, useRef, UserStore } from "@webpack/common";
|
||||
|
||||
import { Review } from "../entities";
|
||||
import { addReview, getReviews, Response, REVIEWS_PER_PAGE } from "../reviewDbApi";
|
||||
@ -28,7 +26,12 @@ import { settings } from "../settings";
|
||||
import { authorize, cl, showToast } from "../utils";
|
||||
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 {
|
||||
discordId: string;
|
||||
@ -113,48 +116,82 @@ function ReviewList({ refetch, reviews, hideOwnReview }: { refetch(): void; revi
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export function ReviewsInputComponent({ discordId, isAuthor, refetch, name }: { discordId: string, name: string; isAuthor: boolean; refetch(): void; }) {
|
||||
const { token } = settings.store;
|
||||
const editorRef = useRef<any>(null);
|
||||
const inputType = InputTypes.FORM;
|
||||
inputType.disableAutoFocus = true;
|
||||
|
||||
function onKeyPress({ key, target }: KeyboardEvent<HTMLTextAreaElement>) {
|
||||
if (key === "Enter") {
|
||||
addReview({
|
||||
userid: discordId,
|
||||
comment: (target as HTMLInputElement).value,
|
||||
star: -1
|
||||
}).then(res => {
|
||||
if (res?.success) {
|
||||
(target as HTMLInputElement).value = ""; // clear the input
|
||||
refetch();
|
||||
} else if (res?.message) {
|
||||
showToast(res.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
const channel = {
|
||||
flags_: 256,
|
||||
guild_id_: null,
|
||||
id: "0",
|
||||
getGuildId: () => null,
|
||||
isPrivate: () => true,
|
||||
isActiveThread: () => false,
|
||||
isArchivedLockedThread: () => false,
|
||||
isDM: () => true,
|
||||
roles: { "0": { permissions: 0n } },
|
||||
getRecipientId: () => "0",
|
||||
hasFlag: () => false,
|
||||
};
|
||||
|
||||
return (
|
||||
<textarea
|
||||
className={classes(Classes.inputDefault, "enter-comment", cl("input"))}
|
||||
onKeyDownCapture={e => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault(); // prevent newlines
|
||||
}
|
||||
}}
|
||||
placeholder={
|
||||
!token
|
||||
? "You need to authorize to review users!"
|
||||
: isAuthor
|
||||
? `Update review for @${name}`
|
||||
: `Review @${name}`
|
||||
}
|
||||
onKeyDown={onKeyPress}
|
||||
onClick={() => {
|
||||
<>
|
||||
<div onClick={() => {
|
||||
if (!token) {
|
||||
showToast("Opening authorization window...");
|
||||
authorize();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}}>
|
||||
<InputComponent
|
||||
className={cl("input")}
|
||||
channel={channel}
|
||||
placeholder={
|
||||
!token
|
||||
? "You need to authorize to review users!"
|
||||
: isAuthor
|
||||
? `Update review for @${name}`
|
||||
: `Review @${name}`
|
||||
}
|
||||
type={inputType}
|
||||
disableThemedBackground={true}
|
||||
setEditorRef={ref => editorRef.current = ref}
|
||||
textValue=""
|
||||
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
|
||||
}
|
||||
|
||||
export const enum NotificationType {
|
||||
Info = 0,
|
||||
Ban = 1,
|
||||
Unban = 2,
|
||||
Warning = 3
|
||||
}
|
||||
|
||||
export interface Badge {
|
||||
name: string;
|
||||
description: string;
|
||||
@ -45,6 +52,13 @@ export interface BanInfo {
|
||||
banEndDate: number;
|
||||
}
|
||||
|
||||
export interface Notification {
|
||||
id: number;
|
||||
title: string;
|
||||
content: string;
|
||||
type: NotificationType;
|
||||
}
|
||||
|
||||
export interface ReviewDBUser {
|
||||
ID: number;
|
||||
discordID: string;
|
||||
@ -54,6 +68,7 @@ export interface ReviewDBUser {
|
||||
warningCount: number;
|
||||
badges: any[];
|
||||
banInfo: BanInfo | null;
|
||||
notification: Notification | null;
|
||||
lastReviewID: number;
|
||||
type: UserType;
|
||||
}
|
||||
|
@ -24,13 +24,13 @@ import ExpandableHeader from "@components/ExpandableHeader";
|
||||
import { OpenExternalIcon } from "@components/Icons";
|
||||
import { Devs } from "@utils/constants";
|
||||
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 { openReviewsModal } from "./components/ReviewModal";
|
||||
import ReviewsView from "./components/ReviewsView";
|
||||
import { UserType } from "./entities";
|
||||
import { getCurrentUserInfo } from "./reviewDbApi";
|
||||
import { NotificationType } from "./entities";
|
||||
import { getCurrentUserInfo, readNotification } from "./reviewDbApi";
|
||||
import { settings } from "./settings";
|
||||
import { showToast } from "./utils";
|
||||
|
||||
@ -78,40 +78,33 @@ export default definePlugin({
|
||||
|
||||
addContextMenuPatch("guild-header-popout", guildPopoutPatch);
|
||||
|
||||
if (user.banInfo) {
|
||||
const endDate = new Date(user.banInfo.banEndDate);
|
||||
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",
|
||||
confirmText: "Ok",
|
||||
onCancel: () =>
|
||||
VencordNative.native.openExternal(
|
||||
"https://reviewdb.mantikafasi.dev/api/redirect?"
|
||||
+ new URLSearchParams({
|
||||
token: settings.store.token!,
|
||||
page: "dashboard/appeal"
|
||||
})
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
if (user.notification) {
|
||||
const props = user.notification.type === NotificationType.Ban ? {
|
||||
cancelText: "Appeal",
|
||||
confirmText: "Ok",
|
||||
onCancel: () =>
|
||||
VencordNative.native.openExternal(
|
||||
"https://reviewdb.mantikafasi.dev/api/redirect?"
|
||||
+ new URLSearchParams({
|
||||
token: settings.store.token!,
|
||||
page: "dashboard/appeal"
|
||||
})
|
||||
)
|
||||
} : {};
|
||||
|
||||
Alerts.show({
|
||||
title: user.notification.title,
|
||||
body: (
|
||||
Parser.parse(
|
||||
user.notification.content,
|
||||
false
|
||||
)
|
||||
),
|
||||
...props
|
||||
});
|
||||
|
||||
readNotification(user.notification.id);
|
||||
}
|
||||
s.user = user;
|
||||
}, 4000);
|
||||
},
|
||||
|
@ -140,3 +140,12 @@ export function getCurrentUserInfo(token: string): Promise<ReviewDBUser> {
|
||||
method: "POST",
|
||||
}).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;
|
||||
background: transparent;
|
||||
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 {
|
||||
@ -24,13 +33,12 @@
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.vc-rdb-modal-footer {
|
||||
padding: 0;
|
||||
.vc-rdb-input * {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.vc-rdb-modal-footer > div {
|
||||
width: 100%;
|
||||
margin: 6px 16px;
|
||||
.vc-rdb-modal-footer {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.vc-rdb-modal-footer .vc-rdb-input {
|
||||
@ -49,3 +57,20 @@
|
||||
.vc-rdb-modal-reviews {
|
||||
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;
|
||||
}
|
||||
|
@ -145,7 +145,7 @@ export default definePlugin({
|
||||
find: "window.webkitAudioContext",
|
||||
replacement: {
|
||||
match: /(?<=\i\.acquire=function\((\i)\)\{return )navigator\.mediaDevices\.getUserMedia\(\1\)(?=\})/,
|
||||
replace: m => `${m}.then(stream => $self.connectRnnoise(stream))`
|
||||
replace: "$&.then(stream => $self.connectRnnoise(stream, $1.audio))"
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -182,7 +182,8 @@ export default definePlugin({
|
||||
|
||||
setEnabled,
|
||||
isEnabled: () => settings.store.isEnabled,
|
||||
async connectRnnoise(stream: MediaStream): Promise<MediaStream> {
|
||||
async connectRnnoise(stream: MediaStream, isAudio: boolean): Promise<MediaStream> {
|
||||
if (!isAudio) return stream;
|
||||
if (!settings.store.isEnabled) return stream;
|
||||
|
||||
const audioCtx = new AudioContext();
|
||||
|
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)
|
40
src/plugins/serverProfile/index.tsx
Normal file
40
src/plugins/serverProfile/index.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import { Menu } from "@webpack/common";
|
||||
import { Guild } from "discord-types/general";
|
||||
|
||||
import { openGuildProfileModal } from "./GuildProfileModal";
|
||||
|
||||
const Patch: NavContextMenuPatchCallback = (children, { guild }: { guild: Guild; }) => () => {
|
||||
const group = findGroupChildrenByChildId("privacy", children);
|
||||
|
||||
group?.push(
|
||||
<Menu.MenuItem
|
||||
id="vc-server-profile"
|
||||
label="Server Profile"
|
||||
action={() => openGuildProfileModal(guild)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default definePlugin({
|
||||
name: "ServerProfile",
|
||||
description: "Allows you to view info about a server by right clicking it in the server list",
|
||||
authors: [Devs.Ven, Devs.Nuckyz],
|
||||
tags: ["guild", "info"],
|
||||
|
||||
start() {
|
||||
addContextMenuPatch(["guild-context", "guild-header-popout"], Patch);
|
||||
},
|
||||
|
||||
stop() {
|
||||
removeContextMenuPatch(["guild-context", "guild-header-popout"], Patch);
|
||||
}
|
||||
});
|
97
src/plugins/serverProfile/styles.css
Normal file
97
src/plugins/serverProfile/styles.css
Normal file
@ -0,0 +1,97 @@
|
||||
.vc-gp-root {
|
||||
height: 100%;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.vc-gp-banner {
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vc-gp-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
.vc-gp-header img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vc-gp-name-and-description {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2em;
|
||||
}
|
||||
|
||||
.vc-gp-name {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.vc-gp-tab-bar {
|
||||
border-bottom: 2px solid var(--background-modifier-accent);
|
||||
margin: 20px 12px 0;
|
||||
display: flex;
|
||||
gap: 40px;
|
||||
align-items: stretch;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.vc-gp-tab {
|
||||
border-bottom: 2px solid transparent;
|
||||
color: var(--interactive-normal);
|
||||
cursor: pointer;
|
||||
height: 39px;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.vc-gp-tab-content {
|
||||
margin: 1em;
|
||||
}
|
||||
|
||||
.vc-gp-tab:where(.vc-gp-selected, :hover, :focus) {
|
||||
border-bottom-color: var(--interactive-active);
|
||||
}
|
||||
|
||||
.vc-gp-info {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
.vc-gp-server-info-pair {
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.vc-gp-server-info-pair [class^="timestamp"] {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.vc-gp-owner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.2em;
|
||||
}
|
||||
|
||||
.vc-gp-owner img {
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vc-gp-scroller {
|
||||
width: 100%;
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.vc-gp-scroller [class^="listRow"] {
|
||||
margin: 1px 0;
|
||||
}
|
||||
|
||||
.vc-gp-scroller [class^="listRow"]:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user