Compare commits
61 Commits
v1.4.5
...
feat/start
Author | SHA1 | Date | |
---|---|---|---|
|
9bc373d87a | ||
|
fb7e1c64fd | ||
|
1128e8f3ad | ||
|
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 |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@ -38,7 +38,7 @@ jobs:
|
|||||||
run: pnpm build --standalone
|
run: pnpm build --standalone
|
||||||
|
|
||||||
- name: Generate plugin list
|
- name: Generate plugin list
|
||||||
run: pnpm generatePluginJson dist/plugins.json
|
run: pnpm generatePluginJson dist/plugins.json dist/plugin-readmes.json
|
||||||
|
|
||||||
- name: Clean up obsolete files
|
- name: Clean up obsolete files
|
||||||
run: |
|
run: |
|
||||||
|
@ -65,7 +65,7 @@ Also pay attention to the following:
|
|||||||
- Match any but a guaranteed terminating character: `[^;]+`, for example to match the entire assigned value in `var a=b||c||func();`,
|
- Match any but a guaranteed terminating character: `[^;]+`, for example to match the entire assigned value in `var a=b||c||func();`,
|
||||||
`var .{1,2}=([^;]+);`
|
`var .{1,2}=([^;]+);`
|
||||||
- If you don't care about that part, just match a bunch of chars: `.{0,50}`, for example to extract the variable "b" in `createElement("div",{a:"foo",c:"bar"},b)`, `createElement\("div".{0,30},(.{1,2})\),`. Note the `.{0,30}`, this is essentially the same as `.+`, but safer as you can't end up accidently eating thousands of characters
|
- If you don't care about that part, just match a bunch of chars: `.{0,50}`, for example to extract the variable "b" in `createElement("div",{a:"foo",c:"bar"},b)`, `createElement\("div".{0,30},(.{1,2})\),`. Note the `.{0,30}`, this is essentially the same as `.+`, but safer as you can't end up accidently eating thousands of characters
|
||||||
- Additionally, as you might have noticed, all of the appove approaches use regex groups (`(...)`) to capture the variable name. You can then use those groups in your replacement to access those variables dynamically
|
- Additionally, as you might have noticed, all of the above approaches use regex groups (`(...)`) to capture the variable name. You can then use those groups in your replacement to access those variables dynamically
|
||||||
|
|
||||||
#### "replace"
|
#### "replace"
|
||||||
|
|
||||||
|
@ -4,8 +4,7 @@
|
|||||||
|
|
||||||
The cutest Discord client mod
|
The cutest Discord client mod
|
||||||
|
|
||||||
![](https://user-images.githubusercontent.com/45497981/235015332-0453d3eb-1da6-4601-963e-ef5e454123a1.png)
|
![image](https://github.com/Vendicated/Vencord/assets/45497981/706722b1-32de-4d99-bee9-93993b504334)
|
||||||
*A screenshot of Vencord featuring the [ClearVision-v6 theme](https://github.com/ClearVision/ClearVision-v6) (Vencord does not come with it pre-installed, it is only an example)*
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@ Then fully close Discord from your taskbar or task manager, and restart it. Venc
|
|||||||
|
|
||||||
If you're using Discord already, go into the `Updater` tab in settings.
|
If you're using Discord already, go into the `Updater` tab in settings.
|
||||||
|
|
||||||
Sometimes it may be neccessary to manually update if the GUI updater fails.
|
Sometimes it may be necessary to manually update if the GUI updater fails.
|
||||||
|
|
||||||
To pull latest changes:
|
To pull latest changes:
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "vencord",
|
"name": "vencord",
|
||||||
"private": "true",
|
"private": "true",
|
||||||
"version": "1.4.5",
|
"version": "1.4.7",
|
||||||
"description": "The cutest Discord client mod",
|
"description": "The cutest Discord client mod",
|
||||||
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
|
@ -19,13 +19,14 @@
|
|||||||
|
|
||||||
import esbuild from "esbuild";
|
import esbuild from "esbuild";
|
||||||
|
|
||||||
import { commonOpts, globPlugins, isStandalone, VERSION, watch } from "./common.mjs";
|
import { BUILD_TIMESTAMP, commonOpts, globPlugins, isStandalone, updaterDisabled, VERSION, watch } from "./common.mjs";
|
||||||
|
|
||||||
const defines = {
|
const defines = {
|
||||||
IS_STANDALONE: isStandalone,
|
IS_STANDALONE: isStandalone,
|
||||||
IS_DEV: JSON.stringify(watch),
|
IS_DEV: JSON.stringify(watch),
|
||||||
|
IS_UPDATER_DISABLED: updaterDisabled,
|
||||||
VERSION: JSON.stringify(VERSION),
|
VERSION: JSON.stringify(VERSION),
|
||||||
BUILD_TIMESTAMP: Date.now(),
|
BUILD_TIMESTAMP,
|
||||||
};
|
};
|
||||||
if (defines.IS_STANDALONE === "false")
|
if (defines.IS_STANDALONE === "false")
|
||||||
// If this is a local build (not standalone), optimise
|
// If this is a local build (not standalone), optimise
|
||||||
|
@ -24,7 +24,7 @@ import { readFileSync } from "fs";
|
|||||||
import { appendFile, mkdir, readFile, rm, writeFile } from "fs/promises";
|
import { appendFile, mkdir, readFile, rm, writeFile } from "fs/promises";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
|
|
||||||
import { commonOpts, globPlugins, VERSION, watch } from "./common.mjs";
|
import { BUILD_TIMESTAMP, commonOpts, globPlugins, VERSION, watch } from "./common.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {esbuild.BuildOptions}
|
* @type {esbuild.BuildOptions}
|
||||||
@ -46,8 +46,9 @@ const commonOptions = {
|
|||||||
IS_DEV: JSON.stringify(watch),
|
IS_DEV: JSON.stringify(watch),
|
||||||
IS_DISCORD_DESKTOP: "false",
|
IS_DISCORD_DESKTOP: "false",
|
||||||
IS_VESKTOP: "false",
|
IS_VESKTOP: "false",
|
||||||
|
IS_UPDATER_DISABLED: "true",
|
||||||
VERSION: JSON.stringify(VERSION),
|
VERSION: JSON.stringify(VERSION),
|
||||||
BUILD_TIMESTAMP: Date.now(),
|
BUILD_TIMESTAMP,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -30,15 +30,18 @@ import PackageJSON from "../../package.json" assert { type: "json" };
|
|||||||
import { getPluginTarget } from "../utils.mjs";
|
import { getPluginTarget } from "../utils.mjs";
|
||||||
|
|
||||||
export const VERSION = PackageJSON.version;
|
export const VERSION = PackageJSON.version;
|
||||||
export const BUILD_TIMESTAMP = Date.now();
|
// https://reproducible-builds.org/docs/source-date-epoch/
|
||||||
|
export const BUILD_TIMESTAMP = Number(process.env.SOURCE_DATE_EPOCH) || Date.now();
|
||||||
export const watch = process.argv.includes("--watch");
|
export const watch = process.argv.includes("--watch");
|
||||||
export const isStandalone = JSON.stringify(process.argv.includes("--standalone"));
|
export const isStandalone = JSON.stringify(process.argv.includes("--standalone"));
|
||||||
export const gitHash = execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
|
export const updaterDisabled = JSON.stringify(process.argv.includes("--disable-updater"));
|
||||||
|
export const gitHash = process.env.VENCORD_HASH || execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
|
||||||
export const banner = {
|
export const banner = {
|
||||||
js: `
|
js: `
|
||||||
// Vencord ${gitHash}
|
// Vencord ${gitHash}
|
||||||
// Standalone: ${isStandalone}
|
// Standalone: ${isStandalone}
|
||||||
// Platform: ${isStandalone === "false" ? process.platform : "Universal"}
|
// Platform: ${isStandalone === "false" ? process.platform : "Universal"}
|
||||||
|
// Updater disabled: ${updaterDisabled}
|
||||||
`.trim()
|
`.trim()
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -133,11 +136,14 @@ export const gitRemotePlugin = {
|
|||||||
namespace: "git-remote", path: args.path
|
namespace: "git-remote", path: args.path
|
||||||
}));
|
}));
|
||||||
build.onLoad({ filter, namespace: "git-remote" }, async () => {
|
build.onLoad({ filter, namespace: "git-remote" }, async () => {
|
||||||
const res = await promisify(exec)("git remote get-url origin", { encoding: "utf-8" });
|
let remote = process.env.VENCORD_REMOTE;
|
||||||
const remote = res.stdout.trim()
|
if (!remote) {
|
||||||
.replace("https://github.com/", "")
|
const res = await promisify(exec)("git remote get-url origin", { encoding: "utf-8" });
|
||||||
.replace("git@github.com:", "")
|
remote = res.stdout.trim()
|
||||||
.replace(/.git$/, "");
|
.replace("https://github.com/", "")
|
||||||
|
.replace("git@github.com:", "")
|
||||||
|
.replace(/.git$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
return { contents: `export default "${remote}"` };
|
return { contents: `export default "${remote}"` };
|
||||||
});
|
});
|
||||||
|
@ -161,11 +161,15 @@ async function parseFile(fileName: string) {
|
|||||||
|
|
||||||
const target = getPluginTarget(fileName);
|
const target = getPluginTarget(fileName);
|
||||||
if (target) {
|
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;
|
data.target = target as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
let readme = "";
|
||||||
|
try {
|
||||||
|
readme = readFileSync(join(fileName, "..", "README.md"), "utf-8");
|
||||||
|
} catch { }
|
||||||
|
return [data, readme] as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw fail("no default export called 'definePlugin' found");
|
throw fail("no default export called 'definePlugin' found");
|
||||||
@ -194,18 +198,24 @@ function isPluginFile({ name }: { name: string; }) {
|
|||||||
(async () => {
|
(async () => {
|
||||||
parseDevs();
|
parseDevs();
|
||||||
|
|
||||||
const plugins = ["src/plugins", "src/plugins/_core"].flatMap(dir =>
|
const plugins = [] as PluginData[];
|
||||||
|
const readmes = {} as Record<string, string>;
|
||||||
|
|
||||||
|
await Promise.all(["src/plugins", "src/plugins/_core"].flatMap(dir =>
|
||||||
readdirSync(dir, { withFileTypes: true })
|
readdirSync(dir, { withFileTypes: true })
|
||||||
.filter(isPluginFile)
|
.filter(isPluginFile)
|
||||||
.map(async dirent =>
|
.map(async dirent => {
|
||||||
parseFile(await getEntryPoint(dir, dirent))
|
const [data, readme] = await parseFile(await getEntryPoint(dir, dirent));
|
||||||
)
|
plugins.push(data);
|
||||||
);
|
if (readme) readmes[data.name] = readme;
|
||||||
|
})
|
||||||
|
));
|
||||||
|
|
||||||
const data = JSON.stringify(await Promise.all(plugins));
|
const data = JSON.stringify(plugins);
|
||||||
|
|
||||||
if (process.argv.length > 2) {
|
if (process.argv.length > 3) {
|
||||||
writeFileSync(process.argv[2], data);
|
writeFileSync(process.argv[2], data);
|
||||||
|
writeFileSync(process.argv[3], JSON.stringify(readmes));
|
||||||
} else {
|
} else {
|
||||||
console.log(data);
|
console.log(data);
|
||||||
}
|
}
|
||||||
|
@ -171,7 +171,7 @@ page.on("console", async e => {
|
|||||||
plugin,
|
plugin,
|
||||||
type,
|
type,
|
||||||
id,
|
id,
|
||||||
match: regex,
|
match: regex.replace(/\[A-Za-z_\$\]\[\\w\$\]\*/g, "\\i"),
|
||||||
error: cause
|
error: cause
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
*/
|
*/
|
||||||
export function getPluginTarget(filePath) {
|
export function getPluginTarget(filePath) {
|
||||||
const pathParts = filePath.split(/[/\\]/);
|
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 identifier = pathParts.at(-1).replace(/\.tsx?$/, "");
|
||||||
const identiferBits = identifier.split(".");
|
const identiferBits = identifier.split(".");
|
||||||
|
@ -48,7 +48,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
addThemeChangeListener(cb: () => void) {
|
addThemeChangeListener(cb: () => void) {
|
||||||
ipcRenderer.on(IpcEvents.THEME_UPDATE, cb);
|
ipcRenderer.on(IpcEvents.THEME_UPDATE, () => cb());
|
||||||
},
|
},
|
||||||
|
|
||||||
openFile: () => invoke<void>(IpcEvents.OPEN_QUICKCSS),
|
openFile: () => invoke<void>(IpcEvents.OPEN_QUICKCSS),
|
||||||
|
21
src/components/CodeBlock.tsx
Normal file
21
src/components/CodeBlock.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { findByPropsLazy } from "@webpack";
|
||||||
|
import { Parser } from "@webpack/common";
|
||||||
|
|
||||||
|
const CodeContainerClasses = findByPropsLazy("markup", "codeContainer");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders code in a Discord codeblock
|
||||||
|
*/
|
||||||
|
export function CodeBlock(props: { content?: string, lang: string; }) {
|
||||||
|
return (
|
||||||
|
<div className={CodeContainerClasses.markup}>
|
||||||
|
{Parser.defaultRules.codeBlock.react(props, null, {})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
113
src/components/PluginSettings/ContributorModal.tsx
Normal file
113
src/components/PluginSettings/ContributorModal.tsx
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "./contributorModal.css";
|
||||||
|
|
||||||
|
import { useSettings } from "@api/Settings";
|
||||||
|
import { classNameFactory } from "@api/Styles";
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { DevsById } from "@utils/constants";
|
||||||
|
import { fetchUserProfile, getTheme, Theme } from "@utils/discord";
|
||||||
|
import { ModalContent, ModalRoot, openModal } from "@utils/modal";
|
||||||
|
import { Forms, MaskedLink, showToast, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common";
|
||||||
|
import { User } from "discord-types/general";
|
||||||
|
|
||||||
|
import Plugins from "~plugins";
|
||||||
|
|
||||||
|
import { PluginCard } from ".";
|
||||||
|
|
||||||
|
const WebsiteIconDark = "/assets/e1e96d89e192de1997f73730db26e94f.svg";
|
||||||
|
const WebsiteIconLight = "/assets/730f58bcfd5a57a5e22460c445a0c6cf.svg";
|
||||||
|
const GithubIconLight = "/assets/3ff98ad75ac94fa883af5ed62d17c459.svg";
|
||||||
|
const GithubIconDark = "/assets/6a853b4c87fce386cbfef4a2efbacb09.svg";
|
||||||
|
|
||||||
|
const cl = classNameFactory("vc-author-modal-");
|
||||||
|
|
||||||
|
export function openContributorModal(user: User) {
|
||||||
|
openModal(modalProps =>
|
||||||
|
<ModalRoot {...modalProps}>
|
||||||
|
<ErrorBoundary>
|
||||||
|
<ModalContent className={cl("root")}>
|
||||||
|
<ContributorModal user={user} />
|
||||||
|
</ModalContent>
|
||||||
|
</ErrorBoundary>
|
||||||
|
</ModalRoot>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function GithubIcon() {
|
||||||
|
const src = getTheme() === Theme.Light ? GithubIconLight : GithubIconDark;
|
||||||
|
return <img src={src} alt="GitHub" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function WebsiteIcon() {
|
||||||
|
const src = getTheme() === Theme.Light ? WebsiteIconLight : WebsiteIconDark;
|
||||||
|
return <img src={src} alt="Website" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContributorModal({ user }: { user: User; }) {
|
||||||
|
useSettings();
|
||||||
|
|
||||||
|
const profile = useStateFromStores([UserProfileStore], () => UserProfileStore.getUserProfile(user.id));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!profile && !user.bot && user.id)
|
||||||
|
fetchUserProfile(user.id);
|
||||||
|
}, [user.id]);
|
||||||
|
|
||||||
|
const githubName = profile?.connectedAccounts?.find(a => a.type === "github")?.name;
|
||||||
|
const website = profile?.connectedAccounts?.find(a => a.type === "domain")?.name;
|
||||||
|
|
||||||
|
const plugins = useMemo(() => {
|
||||||
|
const allPlugins = Object.values(Plugins);
|
||||||
|
const pluginsByAuthor = DevsById[user.id]
|
||||||
|
? allPlugins.filter(p => p.authors.includes(DevsById[user.id]))
|
||||||
|
: allPlugins.filter(p => p.authors.some(a => a.name === user.username));
|
||||||
|
|
||||||
|
return pluginsByAuthor
|
||||||
|
.filter(p => !p.name.endsWith("API"))
|
||||||
|
.sort((a, b) => Number(a.required ?? false) - Number(b.required ?? false));
|
||||||
|
}, [user.id, user.username]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={cl("header")}>
|
||||||
|
<img
|
||||||
|
className={cl("avatar")}
|
||||||
|
src={user.getAvatarURL(void 0, 512, true)}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<Forms.FormTitle tag="h2" className={cl("name")}>{user.username}</Forms.FormTitle>
|
||||||
|
|
||||||
|
<div className={cl("links")}>
|
||||||
|
{website && (
|
||||||
|
<MaskedLink
|
||||||
|
href={"https://" + website}
|
||||||
|
>
|
||||||
|
<WebsiteIcon />
|
||||||
|
</MaskedLink>
|
||||||
|
)}
|
||||||
|
{githubName && (
|
||||||
|
<MaskedLink href={`https://github.com/${githubName}`}>
|
||||||
|
<GithubIcon />
|
||||||
|
</MaskedLink>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={cl("plugins")}>
|
||||||
|
{plugins.map(p =>
|
||||||
|
<PluginCard
|
||||||
|
key={p.name}
|
||||||
|
plugin={p}
|
||||||
|
disabled={p.required ?? false}
|
||||||
|
onRestartNeeded={() => showToast("Restart to apply changes!")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -18,17 +18,16 @@
|
|||||||
|
|
||||||
import { generateId } from "@api/Commands";
|
import { generateId } from "@api/Commands";
|
||||||
import { useSettings } from "@api/Settings";
|
import { useSettings } from "@api/Settings";
|
||||||
import { disableStyle, enableStyle } from "@api/Styles";
|
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Flex } from "@components/Flex";
|
import { Flex } from "@components/Flex";
|
||||||
import { proxyLazy } from "@utils/lazy";
|
import { proxyLazy } from "@utils/lazy";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { classes } from "@utils/misc";
|
import { classes, isObjectEmpty } from "@utils/misc";
|
||||||
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
|
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
|
||||||
import { LazyComponent } from "@utils/react";
|
import { LazyComponent } from "@utils/react";
|
||||||
import { OptionType, Plugin } from "@utils/types";
|
import { OptionType, Plugin } from "@utils/types";
|
||||||
import { findByCode, findByPropsLazy } from "@webpack";
|
import { findByCode, findByPropsLazy } from "@webpack";
|
||||||
import { Button, FluxDispatcher, Forms, React, Text, Tooltip, UserStore, UserUtils } from "@webpack/common";
|
import { Button, Clickable, FluxDispatcher, Forms, React, Text, Tooltip, UserStore, UserUtils } from "@webpack/common";
|
||||||
import { User } from "discord-types/general";
|
import { User } from "discord-types/general";
|
||||||
import { Constructor } from "type-fest";
|
import { Constructor } from "type-fest";
|
||||||
|
|
||||||
@ -41,7 +40,7 @@ import {
|
|||||||
SettingSliderComponent,
|
SettingSliderComponent,
|
||||||
SettingTextComponent
|
SettingTextComponent
|
||||||
} from "./components";
|
} from "./components";
|
||||||
import hideBotTagStyle from "./userPopoutHideBotTag.css?managed";
|
import { openContributorModal } from "./ContributorModal";
|
||||||
|
|
||||||
const UserSummaryItem = LazyComponent(() => findByCode("defaultRenderUser", "showDefaultAvatarsForNullUsers"));
|
const UserSummaryItem = LazyComponent(() => findByCode("defaultRenderUser", "showDefaultAvatarsForNullUsers"));
|
||||||
const AvatarStyles = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar");
|
const AvatarStyles = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar");
|
||||||
@ -89,30 +88,19 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||||||
|
|
||||||
const canSubmit = () => Object.values(errors).every(e => !e);
|
const canSubmit = () => Object.values(errors).every(e => !e);
|
||||||
|
|
||||||
const hasSettings = Boolean(pluginSettings && plugin.options);
|
const hasSettings = Boolean(pluginSettings && plugin.options && !isObjectEmpty(plugin.options));
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
enableStyle(hideBotTagStyle);
|
|
||||||
|
|
||||||
let originalUser: User;
|
|
||||||
(async () => {
|
(async () => {
|
||||||
for (const user of plugin.authors.slice(0, 6)) {
|
for (const user of plugin.authors.slice(0, 6)) {
|
||||||
const author = user.id
|
const author = user.id
|
||||||
? await UserUtils.fetchUser(`${user.id}`)
|
? await UserUtils.fetchUser(`${user.id}`)
|
||||||
// only show name & pfp and no actions so users cannot harass plugin devs for support (send dms, add as friend, etc)
|
|
||||||
.then(u => (originalUser = u, makeDummyUser(u)))
|
|
||||||
.catch(() => makeDummyUser({ username: user.name }))
|
.catch(() => makeDummyUser({ username: user.name }))
|
||||||
: makeDummyUser({ username: user.name });
|
: makeDummyUser({ username: user.name });
|
||||||
|
|
||||||
setAuthors(a => [...a, author]);
|
setAuthors(a => [...a, author]);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return () => {
|
|
||||||
disableStyle(hideBotTagStyle);
|
|
||||||
if (originalUser)
|
|
||||||
FluxDispatcher.dispatch({ type: "USER_UPDATE", user: originalUser });
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
async function saveAndClose() {
|
async function saveAndClose() {
|
||||||
@ -214,6 +202,19 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||||||
showDefaultAvatarsForNullUsers
|
showDefaultAvatarsForNullUsers
|
||||||
showUserPopout
|
showUserPopout
|
||||||
renderMoreUsers={renderMoreUsers}
|
renderMoreUsers={renderMoreUsers}
|
||||||
|
renderUser={(user: User) => (
|
||||||
|
<Clickable
|
||||||
|
className={AvatarStyles.clickableAvatar}
|
||||||
|
onClick={() => openContributorModal(user)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className={AvatarStyles.avatar}
|
||||||
|
src={user.getAvatarURL(void 0, 80, true)}
|
||||||
|
alt={user.username}
|
||||||
|
title={user.username}
|
||||||
|
/>
|
||||||
|
</Clickable>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Forms.FormSection>
|
</Forms.FormSection>
|
||||||
|
@ -33,11 +33,10 @@ export function SettingTextComponent({ option, pluginSettings, definedSettings,
|
|||||||
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
||||||
if (typeof isValid === "string") setError(isValid);
|
if (typeof isValid === "string") setError(isValid);
|
||||||
else if (!isValid) setError("Invalid input provided.");
|
else if (!isValid) setError("Invalid input provided.");
|
||||||
else {
|
else setError(null);
|
||||||
setError(null);
|
|
||||||
setState(newValue);
|
setState(newValue);
|
||||||
onChange(newValue);
|
onChange(newValue);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
57
src/components/PluginSettings/contributorModal.css
Normal file
57
src/components/PluginSettings/contributorModal.css
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
.vc-author-modal-root {
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-author-modal-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-author-modal-name {
|
||||||
|
text-transform: none;
|
||||||
|
flex-grow: 0;
|
||||||
|
background: var(--background-tertiary);
|
||||||
|
border-radius: 0 9999px 9999px 0;
|
||||||
|
padding: 6px 0.8em 6px 0.5em;
|
||||||
|
font-size: 20px;
|
||||||
|
height: 20px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-author-modal-name::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
width: 16px;
|
||||||
|
background: var(--background-tertiary);
|
||||||
|
z-index: -1;
|
||||||
|
left: -16px;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-author-modal-avatar {
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-author-modal-links {
|
||||||
|
margin-left: auto;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-author-modal-links img {
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 4px solid var(--background-tertiary);
|
||||||
|
box-sizing: border-box
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-author-modal-plugins {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.5em;
|
||||||
|
}
|
@ -28,7 +28,7 @@ import { SettingsTab } from "@components/VencordSettings/shared";
|
|||||||
import { ChangeList } from "@utils/ChangeList";
|
import { ChangeList } from "@utils/ChangeList";
|
||||||
import { Logger } from "@utils/Logger";
|
import { Logger } from "@utils/Logger";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { classes } from "@utils/misc";
|
import { classes, isObjectEmpty } from "@utils/misc";
|
||||||
import { openModalLazy } from "@utils/modal";
|
import { openModalLazy } from "@utils/modal";
|
||||||
import { LazyComponent, useAwaiter } from "@utils/react";
|
import { LazyComponent, useAwaiter } from "@utils/react";
|
||||||
import { Plugin } from "@utils/types";
|
import { Plugin } from "@utils/types";
|
||||||
@ -91,7 +91,7 @@ interface PluginCardProps extends React.HTMLProps<HTMLDivElement> {
|
|||||||
isNew?: boolean;
|
isNew?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave, isNew }: PluginCardProps) {
|
export function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave, isNew }: PluginCardProps) {
|
||||||
const settings = Settings.plugins[plugin.name];
|
const settings = Settings.plugins[plugin.name];
|
||||||
|
|
||||||
const isEnabled = () => settings.enabled ?? false;
|
const isEnabled = () => settings.enabled ?? false;
|
||||||
@ -161,7 +161,7 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe
|
|||||||
onMouseLeave={onMouseLeave}
|
onMouseLeave={onMouseLeave}
|
||||||
infoButton={
|
infoButton={
|
||||||
<button role="switch" onClick={() => openModal()} className={classes(ButtonClasses.button, cl("info-button"))}>
|
<button role="switch" onClick={() => openModal()} className={classes(ButtonClasses.button, cl("info-button"))}>
|
||||||
{plugin.options
|
{plugin.options && !isObjectEmpty(plugin.options)
|
||||||
? <CogWheel />
|
? <CogWheel />
|
||||||
: <InfoIcon width="24" height="24" />}
|
: <InfoIcon width="24" height="24" />}
|
||||||
</button>
|
</button>
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
[class|="userPopoutOuter"] [class*="botTag"] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
@ -17,6 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { CheckedTextInput } from "@components/CheckedTextInput";
|
import { CheckedTextInput } from "@components/CheckedTextInput";
|
||||||
|
import { CodeBlock } from "@components/CodeBlock";
|
||||||
import { debounce } from "@utils/debounce";
|
import { debounce } from "@utils/debounce";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
|
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
|
||||||
@ -299,7 +300,7 @@ function PatchHelper() {
|
|||||||
{!!(find && match && replacement) && (
|
{!!(find && match && replacement) && (
|
||||||
<>
|
<>
|
||||||
<Forms.FormTitle className={Margins.top20}>Code</Forms.FormTitle>
|
<Forms.FormTitle className={Margins.top20}>Code</Forms.FormTitle>
|
||||||
<div style={{ userSelect: "text" }}>{Parser.parse(makeCodeblock(code, "ts"))}</div>
|
<CodeBlock lang="js" content={code} />
|
||||||
<Button onClick={() => Clipboard.copy(code)}>Copy to Clipboard</Button>
|
<Button onClick={() => Clipboard.copy(code)}>Copy to Clipboard</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -231,7 +231,7 @@ function ThemesTab() {
|
|||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
onChange={onFileUpload}
|
onChange={onFileUpload}
|
||||||
multiple={true}
|
multiple={true}
|
||||||
filters={[{ extensions: ["*.css"] }]}
|
filters={[{ extensions: ["css"] }]}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
|
@ -249,4 +249,4 @@ function Updater() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default IS_WEB ? null : wrapTab(Updater, "Updater");
|
export default IS_UPDATER_DISABLED ? null : wrapTab(Updater, "Updater");
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
transition: 0.1s ease-out;
|
transition: 0.1s ease-out;
|
||||||
transition-property: box-shadow, transform, background, opacity;
|
transition-property: box-shadow, transform, background, opacity;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-addon-card-disabled {
|
.vc-addon-card-disabled {
|
||||||
|
1
src/globals.d.ts
vendored
1
src/globals.d.ts
vendored
@ -35,6 +35,7 @@ declare global {
|
|||||||
export var IS_WEB: boolean;
|
export var IS_WEB: boolean;
|
||||||
export var IS_DEV: boolean;
|
export var IS_DEV: boolean;
|
||||||
export var IS_STANDALONE: boolean;
|
export var IS_STANDALONE: boolean;
|
||||||
|
export var IS_UPDATER_DISABLED: boolean;
|
||||||
export var IS_DISCORD_DESKTOP: boolean;
|
export var IS_DISCORD_DESKTOP: boolean;
|
||||||
export var IS_VESKTOP: boolean;
|
export var IS_VESKTOP: boolean;
|
||||||
export var VERSION: string;
|
export var VERSION: string;
|
||||||
|
@ -22,6 +22,28 @@ import { readFile } from "fs/promises";
|
|||||||
import { request } from "https";
|
import { request } from "https";
|
||||||
import { basename, normalize } from "path";
|
import { basename, normalize } from "path";
|
||||||
|
|
||||||
|
import { getSettings } from "./ipcMain";
|
||||||
|
|
||||||
|
// FixSpotifyEmbeds
|
||||||
|
app.on("browser-window-created", (_, win) => {
|
||||||
|
win.webContents.on("frame-created", (_, { frame }) => {
|
||||||
|
frame.once("dom-ready", () => {
|
||||||
|
if (frame.url.startsWith("https://open.spotify.com/embed/")) {
|
||||||
|
const settings = getSettings().plugins?.FixSpotifyEmbeds;
|
||||||
|
if (!settings?.enabled) return;
|
||||||
|
|
||||||
|
frame.executeJavaScript(`
|
||||||
|
const original = Audio.prototype.play;
|
||||||
|
Audio.prototype.play = function() {
|
||||||
|
this.volume = ${(settings.volume / 100) || 0.1};
|
||||||
|
return original.apply(this, arguments);
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// #region OpenInApp
|
// #region OpenInApp
|
||||||
// These links don't support CORS, so this has to be native
|
// These links don't support CORS, so this has to be native
|
||||||
const validRedirectUrls = /^https:\/\/(spotify\.link|s\.team)\/.+$/;
|
const validRedirectUrls = /^https:\/\/(spotify\.link|s\.team)\/.+$/;
|
||||||
|
@ -20,18 +20,6 @@ import { app } from "electron";
|
|||||||
import { existsSync, mkdirSync, readdirSync, renameSync, statSync, writeFileSync } from "fs";
|
import { existsSync, mkdirSync, readdirSync, renameSync, statSync, writeFileSync } from "fs";
|
||||||
import { basename, dirname, join } from "path";
|
import { basename, dirname, join } from "path";
|
||||||
|
|
||||||
const { setAppUserModelId } = app;
|
|
||||||
|
|
||||||
// Apparently requiring Discords updater too early leads into issues,
|
|
||||||
// copied this workaround from powerCord
|
|
||||||
app.setAppUserModelId = function (id: string) {
|
|
||||||
app.setAppUserModelId = setAppUserModelId;
|
|
||||||
|
|
||||||
setAppUserModelId.call(this, id);
|
|
||||||
|
|
||||||
patchUpdater();
|
|
||||||
};
|
|
||||||
|
|
||||||
function isNewer($new: string, old: string) {
|
function isNewer($new: string, old: string) {
|
||||||
const newParts = $new.slice(4).split(".").map(Number);
|
const newParts = $new.slice(4).split(".").map(Number);
|
||||||
const oldParts = old.slice(4).split(".").map(Number);
|
const oldParts = old.slice(4).split(".").map(Number);
|
||||||
@ -77,25 +65,6 @@ function patchLatest() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Windows Host Updates install to a new folder app-{HOST_VERSION}, so we
|
// Try to patch latest on before-quit
|
||||||
// need to reinject
|
// Discord's Win32 updater will call app.quit() on restart and open new version on will-quit
|
||||||
function patchUpdater() {
|
app.on("before-quit", patchLatest);
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -16,4 +16,5 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
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,
|
element: require("@components/VencordSettings/ThemesTab").default,
|
||||||
className: "vc-themes"
|
className: "vc-themes"
|
||||||
},
|
},
|
||||||
!IS_WEB && {
|
!IS_UPDATER_DISABLED && {
|
||||||
section: "VencordUpdater",
|
section: "VencordUpdater",
|
||||||
label: "Updater",
|
label: "Updater",
|
||||||
element: require("@components/VencordSettings/UpdaterTab").default,
|
element: require("@components/VencordSettings/UpdaterTab").default,
|
||||||
|
@ -49,7 +49,7 @@ export default definePlugin({
|
|||||||
replace: "$1,vcProps=$2$3+(vcProps.channel.nsfw?' vc-nsfw-img':'')"
|
replace: "$1,vcProps=$2$3+(vcProps.channel.nsfw?' vc-nsfw-img':'')"
|
||||||
}, {
|
}, {
|
||||||
match: /(\.renderAttachments=.+?(.)=this\.props)(.+?\.embedWrapper)/g,
|
match: /(\.renderAttachments=.+?(.)=this\.props)(.+?\.embedWrapper)/g,
|
||||||
replace: "$1,vcProps=$2$3+(vcProps.channel.nsfw?' vc-nsfw-img':'')"
|
replace: "$1,vcProps=$2$3+(vcProps.nsfw?' vc-nsfw-img':'')"
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -127,6 +127,7 @@ export const defaultRules = [
|
|||||||
"redircnt@yandex.*",
|
"redircnt@yandex.*",
|
||||||
"feature@youtube.com",
|
"feature@youtube.com",
|
||||||
"kw@youtube.com",
|
"kw@youtube.com",
|
||||||
|
"si@youtu.be",
|
||||||
"wt_zmc",
|
"wt_zmc",
|
||||||
"utm_source",
|
"utm_source",
|
||||||
"utm_content",
|
"utm_content",
|
||||||
@ -135,4 +136,5 @@ export const defaultRules = [
|
|||||||
"utm_term",
|
"utm_term",
|
||||||
"si@open.spotify.com",
|
"si@open.spotify.com",
|
||||||
"igshid",
|
"igshid",
|
||||||
|
"share_id@reddit.com",
|
||||||
];
|
];
|
||||||
|
55
src/plugins/copyUserURLs.tsx
Normal file
55
src/plugins/copyUserURLs.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
|
||||||
|
import { LinkIcon } from "@components/Icons";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import definePlugin from "@utils/types";
|
||||||
|
import { Clipboard, Menu } from "@webpack/common";
|
||||||
|
import type { Channel, User } from "discord-types/general";
|
||||||
|
|
||||||
|
interface UserContextProps {
|
||||||
|
channel: Channel;
|
||||||
|
guildId?: string;
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserContextMenuPatch: NavContextMenuPatchCallback = (children, { user }: UserContextProps) => () => {
|
||||||
|
children.push(
|
||||||
|
<Menu.MenuItem
|
||||||
|
id="vc-copy-user-url"
|
||||||
|
label="Copy User URL"
|
||||||
|
action={() => Clipboard.copy(`<https://discord.com/users/${user.id}>`)}
|
||||||
|
icon={LinkIcon}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "CopyUserURLs",
|
||||||
|
authors: [Devs.castdrian],
|
||||||
|
description: "Adds a 'Copy User URL' option to the user context menu.",
|
||||||
|
|
||||||
|
start() {
|
||||||
|
addContextMenuPatch("user-context", UserContextMenuPatch);
|
||||||
|
},
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
removeContextMenuPatch("user-context", UserContextMenuPatch);
|
||||||
|
},
|
||||||
|
});
|
@ -23,10 +23,12 @@ import { Logger } from "@utils/Logger";
|
|||||||
import { closeAllModals } from "@utils/modal";
|
import { closeAllModals } from "@utils/modal";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { maybePromptToUpdate } from "@utils/updater";
|
import { maybePromptToUpdate } from "@utils/updater";
|
||||||
|
import { findByPropsLazy } from "@webpack";
|
||||||
import { FluxDispatcher, NavigationRouter } from "@webpack/common";
|
import { FluxDispatcher, NavigationRouter } from "@webpack/common";
|
||||||
import type { ReactElement } from "react";
|
import type { ReactElement } from "react";
|
||||||
|
|
||||||
const CrashHandlerLogger = new Logger("CrashHandler");
|
const CrashHandlerLogger = new Logger("CrashHandler");
|
||||||
|
const ModalStack = findByPropsLazy("pushLazy", "popAll");
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
const settings = definePluginSettings({
|
||||||
attemptToPreventCrashes: {
|
attemptToPreventCrashes: {
|
||||||
@ -51,8 +53,6 @@ export default definePlugin({
|
|||||||
authors: [Devs.Nuckyz],
|
authors: [Devs.Nuckyz],
|
||||||
enabledByDefault: true,
|
enabledByDefault: true,
|
||||||
|
|
||||||
popAllModals: undefined as (() => void) | undefined,
|
|
||||||
|
|
||||||
settings,
|
settings,
|
||||||
|
|
||||||
patches: [
|
patches: [
|
||||||
@ -62,13 +62,6 @@ export default definePlugin({
|
|||||||
match: /(?=this\.setState\()/,
|
match: /(?=this\.setState\()/,
|
||||||
replace: "$self.handleCrash(this)||"
|
replace: "$self.handleCrash(this)||"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
find: 'dispatch({type:"MODAL_POP_ALL"})',
|
|
||||||
replacement: {
|
|
||||||
match: /"MODAL_POP_ALL".+?};(?<=(\i)=function.+?)/,
|
|
||||||
replace: (m, popAll) => `${m}$self.popAllModals=${popAll};`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
@ -128,7 +121,7 @@ export default definePlugin({
|
|||||||
CrashHandlerLogger.debug("Failed to close open context menu.", err);
|
CrashHandlerLogger.debug("Failed to close open context menu.", err);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
this.popAllModals?.();
|
ModalStack?.popAll();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
CrashHandlerLogger.debug("Failed to close old modals.", err);
|
CrashHandlerLogger.debug("Failed to close old modals.", err);
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { definePluginSettings } from "@api/Settings";
|
import { definePluginSettings, Settings } from "@api/Settings";
|
||||||
import { Link } from "@components/Link";
|
import { Link } from "@components/Link";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { isTruthy } from "@utils/guards";
|
import { isTruthy } from "@utils/guards";
|
||||||
@ -86,8 +86,7 @@ const settings = definePluginSettings({
|
|||||||
appID: {
|
appID: {
|
||||||
type: OptionType.STRING,
|
type: OptionType.STRING,
|
||||||
description: "Application ID (required)",
|
description: "Application ID (required)",
|
||||||
restartNeeded: true,
|
onChange: onChange,
|
||||||
onChange: setRpc,
|
|
||||||
isValid: (value: string) => {
|
isValid: (value: string) => {
|
||||||
if (!value) return "Application ID is required.";
|
if (!value) return "Application ID is required.";
|
||||||
if (value && !/^\d+$/.test(value)) return "Application ID must be a number.";
|
if (value && !/^\d+$/.test(value)) return "Application ID must be a number.";
|
||||||
@ -97,8 +96,7 @@ const settings = definePluginSettings({
|
|||||||
appName: {
|
appName: {
|
||||||
type: OptionType.STRING,
|
type: OptionType.STRING,
|
||||||
description: "Application name (required)",
|
description: "Application name (required)",
|
||||||
restartNeeded: true,
|
onChange: onChange,
|
||||||
onChange: setRpc,
|
|
||||||
isValid: (value: string) => {
|
isValid: (value: string) => {
|
||||||
if (!value) return "Application name is required.";
|
if (!value) return "Application name is required.";
|
||||||
if (value.length > 128) return "Application name must be not longer than 128 characters.";
|
if (value.length > 128) return "Application name must be not longer than 128 characters.";
|
||||||
@ -108,8 +106,7 @@ const settings = definePluginSettings({
|
|||||||
details: {
|
details: {
|
||||||
type: OptionType.STRING,
|
type: OptionType.STRING,
|
||||||
description: "Details (line 1)",
|
description: "Details (line 1)",
|
||||||
restartNeeded: true,
|
onChange: onChange,
|
||||||
onChange: setRpc,
|
|
||||||
isValid: (value: string) => {
|
isValid: (value: string) => {
|
||||||
if (value && value.length > 128) return "Details (line 1) must be not longer than 128 characters.";
|
if (value && value.length > 128) return "Details (line 1) must be not longer than 128 characters.";
|
||||||
return true;
|
return true;
|
||||||
@ -118,8 +115,7 @@ const settings = definePluginSettings({
|
|||||||
state: {
|
state: {
|
||||||
type: OptionType.STRING,
|
type: OptionType.STRING,
|
||||||
description: "State (line 2)",
|
description: "State (line 2)",
|
||||||
restartNeeded: true,
|
onChange: onChange,
|
||||||
onChange: setRpc,
|
|
||||||
isValid: (value: string) => {
|
isValid: (value: string) => {
|
||||||
if (value && value.length > 128) return "State (line 2) must be not longer than 128 characters.";
|
if (value && value.length > 128) return "State (line 2) must be not longer than 128 characters.";
|
||||||
return true;
|
return true;
|
||||||
@ -128,8 +124,7 @@ const settings = definePluginSettings({
|
|||||||
type: {
|
type: {
|
||||||
type: OptionType.SELECT,
|
type: OptionType.SELECT,
|
||||||
description: "Activity type",
|
description: "Activity type",
|
||||||
restartNeeded: true,
|
onChange: onChange,
|
||||||
onChange: setRpc,
|
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
label: "Playing",
|
label: "Playing",
|
||||||
@ -157,16 +152,14 @@ const settings = definePluginSettings({
|
|||||||
streamLink: {
|
streamLink: {
|
||||||
type: OptionType.STRING,
|
type: OptionType.STRING,
|
||||||
description: "Twitch.tv or Youtube.com link (only for Streaming activity type)",
|
description: "Twitch.tv or Youtube.com link (only for Streaming activity type)",
|
||||||
restartNeeded: true,
|
onChange: onChange,
|
||||||
onChange: setRpc,
|
disabled: isStreamLinkDisabled,
|
||||||
isDisabled: isStreamLinkDisabled,
|
|
||||||
isValid: isStreamLinkValid
|
isValid: isStreamLinkValid
|
||||||
},
|
},
|
||||||
timestampMode: {
|
timestampMode: {
|
||||||
type: OptionType.SELECT,
|
type: OptionType.SELECT,
|
||||||
description: "Timestamp mode",
|
description: "Timestamp mode",
|
||||||
restartNeeded: true,
|
onChange: onChange,
|
||||||
onChange: setRpc,
|
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
label: "None",
|
label: "None",
|
||||||
@ -190,9 +183,8 @@ const settings = definePluginSettings({
|
|||||||
startTime: {
|
startTime: {
|
||||||
type: OptionType.NUMBER,
|
type: OptionType.NUMBER,
|
||||||
description: "Start timestamp (only for custom timestamp mode)",
|
description: "Start timestamp (only for custom timestamp mode)",
|
||||||
restartNeeded: true,
|
onChange: onChange,
|
||||||
onChange: setRpc,
|
disabled: isTimestampDisabled,
|
||||||
isDisabled: isTimestampDisabled,
|
|
||||||
isValid: (value: number) => {
|
isValid: (value: number) => {
|
||||||
if (value && value < 0) return "Start timestamp must be greater than 0.";
|
if (value && value < 0) return "Start timestamp must be greater than 0.";
|
||||||
return true;
|
return true;
|
||||||
@ -201,9 +193,8 @@ const settings = definePluginSettings({
|
|||||||
endTime: {
|
endTime: {
|
||||||
type: OptionType.NUMBER,
|
type: OptionType.NUMBER,
|
||||||
description: "End timestamp (only for custom timestamp mode)",
|
description: "End timestamp (only for custom timestamp mode)",
|
||||||
restartNeeded: true,
|
onChange: onChange,
|
||||||
onChange: setRpc,
|
disabled: isTimestampDisabled,
|
||||||
isDisabled: isTimestampDisabled,
|
|
||||||
isValid: (value: number) => {
|
isValid: (value: number) => {
|
||||||
if (value && value < 0) return "End timestamp must be greater than 0.";
|
if (value && value < 0) return "End timestamp must be greater than 0.";
|
||||||
return true;
|
return true;
|
||||||
@ -211,16 +202,14 @@ const settings = definePluginSettings({
|
|||||||
},
|
},
|
||||||
imageBig: {
|
imageBig: {
|
||||||
type: OptionType.STRING,
|
type: OptionType.STRING,
|
||||||
description: "Big image key",
|
description: "Big image key/link",
|
||||||
restartNeeded: true,
|
onChange: onChange,
|
||||||
onChange: setRpc,
|
|
||||||
isValid: isImageKeyValid
|
isValid: isImageKeyValid
|
||||||
},
|
},
|
||||||
imageBigTooltip: {
|
imageBigTooltip: {
|
||||||
type: OptionType.STRING,
|
type: OptionType.STRING,
|
||||||
description: "Big image tooltip",
|
description: "Big image tooltip",
|
||||||
restartNeeded: true,
|
onChange: onChange,
|
||||||
onChange: setRpc,
|
|
||||||
isValid: (value: string) => {
|
isValid: (value: string) => {
|
||||||
if (value && value.length > 128) return "Big image tooltip must be not longer than 128 characters.";
|
if (value && value.length > 128) return "Big image tooltip must be not longer than 128 characters.";
|
||||||
return true;
|
return true;
|
||||||
@ -228,16 +217,14 @@ const settings = definePluginSettings({
|
|||||||
},
|
},
|
||||||
imageSmall: {
|
imageSmall: {
|
||||||
type: OptionType.STRING,
|
type: OptionType.STRING,
|
||||||
description: "Small image key",
|
description: "Small image key/link",
|
||||||
restartNeeded: true,
|
onChange: onChange,
|
||||||
onChange: setRpc,
|
|
||||||
isValid: isImageKeyValid
|
isValid: isImageKeyValid
|
||||||
},
|
},
|
||||||
imageSmallTooltip: {
|
imageSmallTooltip: {
|
||||||
type: OptionType.STRING,
|
type: OptionType.STRING,
|
||||||
description: "Small image tooltip",
|
description: "Small image tooltip",
|
||||||
restartNeeded: true,
|
onChange: onChange,
|
||||||
onChange: setRpc,
|
|
||||||
isValid: (value: string) => {
|
isValid: (value: string) => {
|
||||||
if (value && value.length > 128) return "Small image tooltip must be not longer than 128 characters.";
|
if (value && value.length > 128) return "Small image tooltip must be not longer than 128 characters.";
|
||||||
return true;
|
return true;
|
||||||
@ -246,8 +233,7 @@ const settings = definePluginSettings({
|
|||||||
buttonOneText: {
|
buttonOneText: {
|
||||||
type: OptionType.STRING,
|
type: OptionType.STRING,
|
||||||
description: "Button 1 text",
|
description: "Button 1 text",
|
||||||
restartNeeded: true,
|
onChange: onChange,
|
||||||
onChange: setRpc,
|
|
||||||
isValid: (value: string) => {
|
isValid: (value: string) => {
|
||||||
if (value && value.length > 31) return "Button 1 text must be not longer than 31 characters.";
|
if (value && value.length > 31) return "Button 1 text must be not longer than 31 characters.";
|
||||||
return true;
|
return true;
|
||||||
@ -256,14 +242,12 @@ const settings = definePluginSettings({
|
|||||||
buttonOneURL: {
|
buttonOneURL: {
|
||||||
type: OptionType.STRING,
|
type: OptionType.STRING,
|
||||||
description: "Button 1 URL",
|
description: "Button 1 URL",
|
||||||
restartNeeded: true,
|
onChange: onChange
|
||||||
onChange: setRpc
|
|
||||||
},
|
},
|
||||||
buttonTwoText: {
|
buttonTwoText: {
|
||||||
type: OptionType.STRING,
|
type: OptionType.STRING,
|
||||||
description: "Button 2 text",
|
description: "Button 2 text",
|
||||||
restartNeeded: true,
|
onChange: onChange,
|
||||||
onChange: setRpc,
|
|
||||||
isValid: (value: string) => {
|
isValid: (value: string) => {
|
||||||
if (value && value.length > 31) return "Button 2 text must be not longer than 31 characters.";
|
if (value && value.length > 31) return "Button 2 text must be not longer than 31 characters.";
|
||||||
return true;
|
return true;
|
||||||
@ -272,26 +256,29 @@ const settings = definePluginSettings({
|
|||||||
buttonTwoURL: {
|
buttonTwoURL: {
|
||||||
type: OptionType.STRING,
|
type: OptionType.STRING,
|
||||||
description: "Button 2 URL",
|
description: "Button 2 URL",
|
||||||
restartNeeded: true,
|
onChange: onChange
|
||||||
onChange: setRpc
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function isStreamLinkDisabled(): boolean {
|
function onChange() {
|
||||||
|
setRpc(true);
|
||||||
|
if (Settings.plugins.CustomRPC.enabled) setRpc();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStreamLinkDisabled() {
|
||||||
return settings.store.type !== ActivityType.STREAMING;
|
return settings.store.type !== ActivityType.STREAMING;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isStreamLinkValid(): boolean | string {
|
function isStreamLinkValid(value: string) {
|
||||||
if (settings.store.type === ActivityType.STREAMING && settings.store.streamLink && !/(https?:\/\/(www\.)?(twitch\.tv|youtube\.com)\/\w+)/.test(settings.store.streamLink)) return "Streaming link must be a valid URL.";
|
if (!isStreamLinkDisabled() && !/https?:\/\/(www\.)?(twitch\.tv|youtube\.com)\/\w+/.test(value)) return "Streaming link must be a valid URL.";
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isTimestampDisabled(): boolean {
|
function isTimestampDisabled() {
|
||||||
return settings.store.timestampMode !== TimestampMode.CUSTOM;
|
return settings.store.timestampMode !== TimestampMode.CUSTOM;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isImageKeyValid(value: string) {
|
function isImageKeyValid(value: string) {
|
||||||
if (!/https?:\/\//.test(value)) return true;
|
|
||||||
if (/https?:\/\/(?!i\.)?imgur\.com\//.test(value)) return "Imgur link must be a direct link to the image. (e.g. https://i.imgur.com/...)";
|
if (/https?:\/\/(?!i\.)?imgur\.com\//.test(value)) return "Imgur link must be a direct link to the image. (e.g. https://i.imgur.com/...)";
|
||||||
if (/https?:\/\/(?!media\.)?tenor\.com\//.test(value)) return "Tenor link must be a direct link to the image. (e.g. https://media.tenor.com/...)";
|
if (/https?:\/\/(?!media\.)?tenor\.com\//.test(value)) return "Tenor link must be a direct link to the image. (e.g. https://media.tenor.com/...)";
|
||||||
return true;
|
return true;
|
||||||
@ -342,13 +329,10 @@ async function createActivity(): Promise<Activity | undefined> {
|
|||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
case TimestampMode.CUSTOM:
|
case TimestampMode.CUSTOM:
|
||||||
if (startTime) {
|
if (startTime || endTime) {
|
||||||
activity.timestamps = {
|
activity.timestamps = {};
|
||||||
start: startTime,
|
if (startTime) activity.timestamps.start = startTime;
|
||||||
};
|
if (endTime) activity.timestamps.end = endTime;
|
||||||
if (endTime) {
|
|
||||||
activity.timestamps.end = endTime;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case TimestampMode.NONE:
|
case TimestampMode.NONE:
|
||||||
|
5
src/plugins/dearrow/README.md
Normal file
5
src/plugins/dearrow/README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Dearrow
|
||||||
|
|
||||||
|
Makes YouTube embed titles and thumbnails less sensationalist, powered by [Dearrow](https://dearrow.ajay.app/)
|
||||||
|
|
||||||
|
https://github.com/Vendicated/Vencord/assets/45497981/7bf81108-102d-47c5-8ba5-357db4db1283
|
161
src/plugins/dearrow/index.tsx
Normal file
161
src/plugins/dearrow/index.tsx
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import { Logger } from "@utils/Logger";
|
||||||
|
import definePlugin from "@utils/types";
|
||||||
|
import { Tooltip } from "@webpack/common";
|
||||||
|
import type { Component } from "react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
embed: {
|
||||||
|
rawTitle: string;
|
||||||
|
provider?: {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
thumbnail: {
|
||||||
|
proxyURL: string;
|
||||||
|
};
|
||||||
|
video: {
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
dearrow: {
|
||||||
|
enabled: boolean;
|
||||||
|
oldTitle?: string;
|
||||||
|
oldThumb?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const embedUrlRe = /https:\/\/www\.youtube\.com\/embed\/([a-zA-Z0-9_-]{11})/;
|
||||||
|
|
||||||
|
async function embedDidMount(this: Component<Props>) {
|
||||||
|
try {
|
||||||
|
const { embed } = this.props;
|
||||||
|
if (!embed || embed.dearrow || embed.provider?.name !== "YouTube" || !embed.video?.url) return;
|
||||||
|
|
||||||
|
const videoId = embedUrlRe.exec(embed.video.url)?.[1];
|
||||||
|
if (!videoId) return;
|
||||||
|
|
||||||
|
const res = await fetch(`https://sponsor.ajay.app/api/branding?videoID=${videoId}`);
|
||||||
|
if (!res.ok) return;
|
||||||
|
|
||||||
|
const { titles, thumbnails } = await res.json();
|
||||||
|
|
||||||
|
const hasTitle = titles[0]?.votes >= 0;
|
||||||
|
const hasThumb = thumbnails[0]?.votes >= 0;
|
||||||
|
|
||||||
|
if (!hasTitle && !hasThumb) return;
|
||||||
|
|
||||||
|
embed.dearrow = {
|
||||||
|
enabled: true
|
||||||
|
};
|
||||||
|
|
||||||
|
if (titles[0]?.votes >= 0) {
|
||||||
|
embed.dearrow.oldTitle = embed.rawTitle;
|
||||||
|
embed.rawTitle = titles[0].title;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (thumbnails[0]?.votes >= 0) {
|
||||||
|
embed.dearrow.oldThumb = embed.thumbnail.proxyURL;
|
||||||
|
embed.thumbnail.proxyURL = `https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${thumbnails[0].timestamp}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.forceUpdate();
|
||||||
|
} catch (err) {
|
||||||
|
new Logger("Dearrow").error("Failed to dearrow embed", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function DearrowButton({ component }: { component: Component<Props>; }) {
|
||||||
|
const { embed } = component.props;
|
||||||
|
if (!embed?.dearrow) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip text={embed.dearrow.enabled ? "This embed has been dearrowed, click to restore" : "Click to dearrow"}>
|
||||||
|
{({ onMouseEnter, onMouseLeave }) => (
|
||||||
|
<button
|
||||||
|
onMouseEnter={onMouseEnter}
|
||||||
|
onMouseLeave={onMouseLeave}
|
||||||
|
className={"vc-dearrow-toggle-" + (embed.dearrow.enabled ? "on" : "off")}
|
||||||
|
onClick={() => {
|
||||||
|
const { enabled, oldThumb, oldTitle } = embed.dearrow;
|
||||||
|
embed.dearrow.enabled = !enabled;
|
||||||
|
if (oldTitle) {
|
||||||
|
embed.dearrow.oldTitle = embed.rawTitle;
|
||||||
|
embed.rawTitle = oldTitle;
|
||||||
|
}
|
||||||
|
if (oldThumb) {
|
||||||
|
embed.dearrow.oldThumb = embed.thumbnail.proxyURL;
|
||||||
|
embed.thumbnail.proxyURL = oldThumb;
|
||||||
|
}
|
||||||
|
|
||||||
|
component.forceUpdate();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Dearrow Icon, taken from https://dearrow.ajay.app/logo.svg (and optimised) */}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24px"
|
||||||
|
height="24px"
|
||||||
|
viewBox="0 0 36 36"
|
||||||
|
aria-label="Toggle Dearrow"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="#1213BD"
|
||||||
|
d="M36 18.302c0 4.981-2.46 9.198-5.655 12.462s-7.323 5.152-12.199 5.152s-9.764-1.112-12.959-4.376S0 23.283 0 18.302s2.574-9.38 5.769-12.644S13.271 0 18.146 0s9.394 2.178 12.589 5.442C33.931 8.706 36 13.322 36 18.302z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#88c9f9"
|
||||||
|
d="m 30.394282,18.410186 c 0,3.468849 -1.143025,6.865475 -3.416513,9.137917 -2.273489,2.272442 -5.670115,2.92874 -9.137918,2.92874 -3.467803,0 -6.373515,-1.147212 -8.6470033,-3.419654 -2.2734888,-2.272442 -3.5871299,-5.178154 -3.5871299,-8.647003 0,-3.46885 0.9420533,-6.746149 3.2144954,-9.0196379 2.2724418,-2.2734888 5.5507878,-3.9513905 9.0196378,-3.9513905 3.46885,0 6.492841,1.9322561 8.76633,4.204698 2.273489,2.2724424 3.788101,5.2974804 3.788101,8.7663304 z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#0a62a5"
|
||||||
|
d="m 23.95823,17.818306 c 0,3.153748 -2.644888,5.808102 -5.798635,5.808102 -3.153748,0 -5.599825,-2.654354 -5.599825,-5.808102 0,-3.153747 2.446077,-5.721714 5.599825,-5.721714 3.153747,0 5.798635,2.567967 5.798635,5.721714 z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "Dearrow",
|
||||||
|
description: "Makes YouTube embed titles and thumbnails less sensationalist, powered by Dearrow",
|
||||||
|
authors: [Devs.Ven],
|
||||||
|
|
||||||
|
embedDidMount,
|
||||||
|
renderButton(component: Component<Props>) {
|
||||||
|
return (
|
||||||
|
<ErrorBoundary noop>
|
||||||
|
<DearrowButton component={component} />
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
patches: [{
|
||||||
|
find: "this.renderInlineMediaEmbed",
|
||||||
|
replacement: [
|
||||||
|
// patch componentDidMount to replace embed thumbnail and title
|
||||||
|
{
|
||||||
|
match: /(\i).render=function.{0,50}\i\.embed/,
|
||||||
|
replace: "$1.componentDidMount=$self.embedDidMount,$&"
|
||||||
|
},
|
||||||
|
|
||||||
|
// add dearrow button
|
||||||
|
{
|
||||||
|
match: /children:\[(?=null!=\i\?\i\.renderSuppressButton)/,
|
||||||
|
replace: "children:[$self.renderButton(this),"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}],
|
||||||
|
});
|
12
src/plugins/dearrow/styles.css
Normal file
12
src/plugins/dearrow/styles.css
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
.vc-dearrow-toggle-off svg {
|
||||||
|
filter: grayscale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-dearrow-toggle-on, .vc-dearrow-toggle-off {
|
||||||
|
all: unset;
|
||||||
|
display: inline;
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
top: 0.75rem;
|
||||||
|
right: 0.75rem;
|
||||||
|
}
|
@ -212,15 +212,15 @@ export default definePlugin({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: "canStreamHighQuality:function",
|
find: "canUseHighVideoUploadQuality:function",
|
||||||
predicate: () => settings.store.enableStreamQualityBypass,
|
predicate: () => settings.store.enableStreamQualityBypass,
|
||||||
replacement: [
|
replacement: [
|
||||||
"canUseHighVideoUploadQuality",
|
"canUseHighVideoUploadQuality",
|
||||||
"canStreamHighQuality",
|
// TODO: Remove the last two when they get removed from stable
|
||||||
"canStreamMidQuality"
|
"(?:canStreamQuality|canStreamHighQuality|canStreamMidQuality)",
|
||||||
].map(func => {
|
].map(func => {
|
||||||
return {
|
return {
|
||||||
match: new RegExp(`${func}:function\\(\\i\\){`),
|
match: new RegExp(`${func}:function\\(\\i(?:,\\i)?\\){`, "g"),
|
||||||
replace: "$&return true;"
|
replace: "$&return true;"
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
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)
|
@ -87,7 +87,7 @@ export const settings = definePluginSettings({
|
|||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "FavoriteGifSearch",
|
name: "FavoriteGifSearch",
|
||||||
authors: [Devs.Aria],
|
authors: [Devs.Aria],
|
||||||
description: "Adds a search bar for favorite gifs",
|
description: "Adds a search bar to favorite gifs.",
|
||||||
|
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
26
src/plugins/fixSpotifyEmbeds.desktop.ts
Normal file
26
src/plugins/fixSpotifyEmbeds.desktop.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { definePluginSettings } from "@api/Settings";
|
||||||
|
import { makeRange } from "@components/PluginSettings/components";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
|
|
||||||
|
// The entire code of this plugin can be found in ipcPlugins
|
||||||
|
export default definePlugin({
|
||||||
|
name: "FixSpotifyEmbeds",
|
||||||
|
description: "Fixes spotify embeds being incredibly loud by letting you customise the volume",
|
||||||
|
authors: [Devs.Ven],
|
||||||
|
settings: definePluginSettings({
|
||||||
|
volume: {
|
||||||
|
type: OptionType.SLIDER,
|
||||||
|
description: "The volume % to set for spotify embeds. Anything above 10% is veeeery loud",
|
||||||
|
markers: makeRange(0, 100, 10),
|
||||||
|
stickToMarkers: false,
|
||||||
|
default: 10
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
@ -32,12 +32,18 @@ function makeIcon(showCurrentGame?: boolean) {
|
|||||||
return function () {
|
return function () {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
width="24"
|
width="20"
|
||||||
height="24"
|
height="20"
|
||||||
viewBox="0 96 960 960"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<path fill="currentColor" d="M182 856q-51 0-79-35.5T82 734l42-300q9-60 53.5-99T282 296h396q60 0 104.5 39t53.5 99l42 300q7 51-21 86.5T778 856q-21 0-39-7.5T706 826l-90-90H344l-90 90q-15 15-33 22.5t-39 7.5Zm498-240q17 0 28.5-11.5T720 576q0-17-11.5-28.5T680 536q-17 0-28.5 11.5T640 576q0 17 11.5 28.5T680 616Zm-80-120q17 0 28.5-11.5T640 456q0-17-11.5-28.5T600 416q-17 0-28.5 11.5T560 456q0 17 11.5 28.5T600 496ZM310 616h60v-70h70v-60h-70v-70h-60v70h-70v60h70v70Z" />
|
<path fill="currentColor" mask="url(#gameActivityMask)" d="M3.06 20.4q-1.53 0-2.37-1.065T.06 16.74l1.26-9q.27-1.8 1.605-2.97T6.06 3.6h11.88q1.8 0 3.135 1.17t1.605 2.97l1.26 9q.21 1.53-.63 2.595T20.94 20.4q-.63 0-1.17-.225T18.78 19.5l-2.7-2.7H7.92l-2.7 2.7q-.45.45-.99.675t-1.17.225Zm14.94-7.2q.51 0 .855-.345T19.2 12q0-.51-.345-.855T18 10.8q-.51 0-.855.345T16.8 12q0 .51.345 .855T18 13.2Zm-2.4-3.6q.51 0 .855-.345T16.8 8.4q0-.51-.345-.855T15.6 7.2q-.51 0-.855.345T14.4 8.4q0 .51.345 .855T15.6 9.6ZM6.9 13.2h1.8v-2.1h2.1v-1.8h-2.1v-2.1h-1.8v2.1h-2.1v1.8h2.1v2.1Z" />
|
||||||
{!showCurrentGame && <line x1="920" y1="280" x2="40" y2="880" stroke="var(--status-danger)" stroke-width="80" />}
|
{!showCurrentGame && <>
|
||||||
|
<mask id="gameActivityMask" >
|
||||||
|
<rect fill="white" x="0" y="0" width="24" height="24" />
|
||||||
|
<path fill="black" d="M23.27 4.54 19.46.73 .73 19.46 4.54 23.27 23.27 4.54Z"/>
|
||||||
|
</mask>
|
||||||
|
<path fill="var(--status-danger)" d="M23 2.27 21.73 1 1 21.73 2.27 23 23 2.27Z" />
|
||||||
|
</>}
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -60,7 +66,7 @@ function GameActivityToggleButton() {
|
|||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "GameActivityToggle",
|
name: "GameActivityToggle",
|
||||||
description: "Adds a button next to the mic and deafen button to toggle game activity.",
|
description: "Adds a button next to the mic and deafen button to toggle game activity.",
|
||||||
authors: [Devs.Nuckyz],
|
authors: [Devs.Nuckyz, Devs.RuukuLada],
|
||||||
dependencies: ["SettingsStoreAPI"],
|
dependencies: ["SettingsStoreAPI"],
|
||||||
|
|
||||||
patches: [
|
patches: [
|
||||||
|
@ -49,7 +49,7 @@ export default definePlugin({
|
|||||||
await this.buildCss();
|
await this.buildCss();
|
||||||
|
|
||||||
addButton("HideAttachments", msg => {
|
addButton("HideAttachments", msg => {
|
||||||
if (!msg.attachments.length && !msg.embeds.length) return null;
|
if (!msg.attachments.length && !msg.embeds.length && !msg.stickerItems.length) return null;
|
||||||
|
|
||||||
const isHidden = hiddenMessages.has(msg.id);
|
const isHidden = hiddenMessages.has(msg.id);
|
||||||
|
|
||||||
@ -72,7 +72,7 @@ export default definePlugin({
|
|||||||
async buildCss() {
|
async buildCss() {
|
||||||
const elements = [...hiddenMessages].map(id => `#message-accessories-${id}`).join(",");
|
const elements = [...hiddenMessages].map(id => `#message-accessories-${id}`).join(",");
|
||||||
style.textContent = `
|
style.textContent = `
|
||||||
:is(${elements}) [class*="embedWrapper"] {
|
:is(${elements}) :is([class*="embedWrapper"], [class*="clickableSticker"]) {
|
||||||
/* important is not necessary, but add it to make sure bad themes won't break it */
|
/* important is not necessary, but add it to make sure bad themes won't break it */
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
6
src/plugins/imageZoom/README.md
Normal file
6
src/plugins/imageZoom/README.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# ImageZoom
|
||||||
|
|
||||||
|
Lets you zoom in to images and gifs. Use scroll wheel to zoom in and shift + scroll wheel to increase lens radius / size
|
||||||
|
|
||||||
|
![Example](https://i.imgur.com/VJdo4aq.png)
|
||||||
|
![ContextMenu](https://i.imgur.com/0oaRM2s.png)
|
@ -99,6 +99,15 @@ const imageContextMenuPatch: NavContextMenuPatchCallback = children => () => {
|
|||||||
ContextMenu.close();
|
ContextMenu.close();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Menu.MenuCheckboxItem
|
||||||
|
id="vc-nearest-neighbour"
|
||||||
|
label="Nearset Neighbour"
|
||||||
|
checked={settings.store.nearestNeighbour}
|
||||||
|
action={() => {
|
||||||
|
settings.store.nearestNeighbour = !settings.store.nearestNeighbour;
|
||||||
|
ContextMenu.close();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Menu.MenuControlItem
|
<Menu.MenuControlItem
|
||||||
id="vc-zoom"
|
id="vc-zoom"
|
||||||
label="Zoom"
|
label="Zoom"
|
||||||
|
@ -28,7 +28,7 @@ import { Button, Forms, React, TextInput } from "@webpack/common";
|
|||||||
import { decrypt } from "../index";
|
import { decrypt } from "../index";
|
||||||
|
|
||||||
export function DecModal(props: any) {
|
export function DecModal(props: any) {
|
||||||
const secret: string = props?.message?.content;
|
const encryptedMessage: string = props?.message?.content;
|
||||||
const [password, setPassword] = React.useState("password");
|
const [password, setPassword] = React.useState("password");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -38,9 +38,9 @@ export function DecModal(props: any) {
|
|||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Secret</Forms.FormTitle>
|
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Message with Encryption</Forms.FormTitle>
|
||||||
<TextInput defaultValue={secret} disabled={true}></TextInput>
|
<TextInput defaultValue={encryptedMessage} disabled={true}></TextInput>
|
||||||
<Forms.FormTitle tag="h5">Password</Forms.FormTitle>
|
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Password</Forms.FormTitle>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={{ marginBottom: "20px" }}
|
style={{ marginBottom: "20px" }}
|
||||||
onChange={setPassword}
|
onChange={setPassword}
|
||||||
@ -51,7 +51,7 @@ export function DecModal(props: any) {
|
|||||||
<Button
|
<Button
|
||||||
color={Button.Colors.GREEN}
|
color={Button.Colors.GREEN}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const toSend = decrypt(secret, password, true);
|
const toSend = decrypt(encryptedMessage, password, true);
|
||||||
if (!toSend || !props?.message) return;
|
if (!toSend || !props?.message) return;
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
Vencord.Plugins.plugins.InvisibleChat.buildEmbed(props?.message, toSend);
|
Vencord.Plugins.plugins.InvisibleChat.buildEmbed(props?.message, toSend);
|
||||||
|
@ -225,8 +225,8 @@ export function encrypt(secret: string, password: string, cover: string): string
|
|||||||
return steggo.hide(secret + "\u200b", password, cover);
|
return steggo.hide(secret + "\u200b", password, cover);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function decrypt(secret: string, password: string, removeIndicator: boolean): string {
|
export function decrypt(encrypted: string, password: string, removeIndicator: boolean): string {
|
||||||
const decrypted = steggo.reveal(secret, password);
|
const decrypted = steggo.reveal(encrypted, password);
|
||||||
return removeIndicator ? decrypted.replace("\u200b", "") : decrypted;
|
return removeIndicator ? decrypted.replace("\u200b", "") : decrypted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,6 +72,14 @@ const enum ActivityFlag {
|
|||||||
INSTANCE = 1 << 0,
|
INSTANCE = 1 << 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const enum NameFormat {
|
||||||
|
StatusName = "status-name",
|
||||||
|
ArtistFirst = "artist-first",
|
||||||
|
SongFirst = "song-first",
|
||||||
|
ArtistOnly = "artist",
|
||||||
|
SongOnly = "song"
|
||||||
|
}
|
||||||
|
|
||||||
const applicationId = "1108588077900898414";
|
const applicationId = "1108588077900898414";
|
||||||
const placeholderId = "2a96cbd8b46e442fc41c2b86b821562f";
|
const placeholderId = "2a96cbd8b46e442fc41c2b86b821562f";
|
||||||
|
|
||||||
@ -117,10 +125,37 @@ const settings = definePluginSettings({
|
|||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
statusName: {
|
statusName: {
|
||||||
description: "text shown in status",
|
description: "custom status text",
|
||||||
type: OptionType.STRING,
|
type: OptionType.STRING,
|
||||||
default: "some music",
|
default: "some music",
|
||||||
},
|
},
|
||||||
|
nameFormat: {
|
||||||
|
description: "Show name of song and artist in status name",
|
||||||
|
type: OptionType.SELECT,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: "Use custom status name",
|
||||||
|
value: NameFormat.StatusName,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Use format 'artist - song'",
|
||||||
|
value: NameFormat.ArtistFirst
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Use format 'song - artist'",
|
||||||
|
value: NameFormat.SongFirst
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Use artist name only",
|
||||||
|
value: NameFormat.ArtistOnly
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Use song name only",
|
||||||
|
value: NameFormat.SongOnly
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
useListeningStatus: {
|
useListeningStatus: {
|
||||||
description: 'show "Listening to" status instead of "Playing"',
|
description: 'show "Listening to" status instead of "Playing"',
|
||||||
type: OptionType.BOOLEAN,
|
type: OptionType.BOOLEAN,
|
||||||
@ -140,13 +175,13 @@ const settings = definePluginSettings({
|
|||||||
value: "placeholder"
|
value: "placeholder"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "LastFMRichPresence",
|
name: "LastFMRichPresence",
|
||||||
description: "Little plugin for Last.fm rich presence",
|
description: "Little plugin for Last.fm rich presence",
|
||||||
authors: [Devs.dzshn, Devs.RuiNtD],
|
authors: [Devs.dzshn, Devs.RuiNtD, Devs.blahajZip, Devs.archeruwu],
|
||||||
|
|
||||||
settingsAboutComponent: () => (
|
settingsAboutComponent: () => (
|
||||||
<>
|
<>
|
||||||
@ -267,9 +302,24 @@ export default definePlugin({
|
|||||||
url: `https://www.last.fm/user/${settings.store.username}`,
|
url: `https://www.last.fm/user/${settings.store.username}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const statusName = (() => {
|
||||||
|
switch (settings.store.nameFormat) {
|
||||||
|
case NameFormat.ArtistFirst:
|
||||||
|
return trackData.artist + " - " + trackData.name;
|
||||||
|
case NameFormat.SongFirst:
|
||||||
|
return trackData.name + " - " + trackData.artist;
|
||||||
|
case NameFormat.ArtistOnly:
|
||||||
|
return trackData.artist;
|
||||||
|
case NameFormat.SongOnly:
|
||||||
|
return trackData.name;
|
||||||
|
default:
|
||||||
|
return settings.store.statusName;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
application_id: applicationId,
|
application_id: applicationId,
|
||||||
name: settings.store.statusName,
|
name: statusName,
|
||||||
|
|
||||||
details: trackData.name,
|
details: trackData.name,
|
||||||
state: trackData.artist,
|
state: trackData.artist,
|
||||||
|
@ -56,7 +56,8 @@ const quotes = [
|
|||||||
"hd{b${",
|
"hd{b${",
|
||||||
"<;vqkijbq33271:56<3799?24944:",
|
"<;vqkijbq33271:56<3799?24944:",
|
||||||
"Thof$lu'ofdn,!qsefc'az*bnrcma+&Om{o+iu\"`khct$)bnrd\"bcdoi&",
|
"Thof$lu'ofdn,!qsefc'az*bnrcma+&Om{o+iu\"`khct$)bnrd\"bcdoi&",
|
||||||
"snofplkb{)c'r\"lod'|f*aurv#cpno`abchijklmno"
|
"snofplkb{)c'r\"lod'|f*aurv#cpno`abchijklmno",
|
||||||
|
"Wdn`khc'|f*eghl{%"
|
||||||
];
|
];
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
|
@ -30,6 +30,9 @@ const ChannelMemberStore = findStoreLazy("ChannelMemberStore") as FluxStore & {
|
|||||||
getProps(guildId: string, channelId: string): { groups: { count: number; id: string; }[]; };
|
getProps(guildId: string, channelId: string): { groups: { count: number; id: string; }[]; };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sharedIntlNumberFormat = new Intl.NumberFormat();
|
||||||
|
const numberFormat = (value: number) => sharedIntlNumberFormat.format(value);
|
||||||
|
|
||||||
function MemberCount() {
|
function MemberCount() {
|
||||||
const { id: channelId, guild_id: guildId } = useStateFromStores([SelectedChannelStore], () => getCurrentChannel());
|
const { id: channelId, guild_id: guildId } = useStateFromStores([SelectedChannelStore], () => getCurrentChannel());
|
||||||
const { groups } = useStateFromStores(
|
const { groups } = useStateFromStores(
|
||||||
@ -57,7 +60,7 @@ function MemberCount() {
|
|||||||
alignContent: "center",
|
alignContent: "center",
|
||||||
gap: 0
|
gap: 0
|
||||||
}}>
|
}}>
|
||||||
<Tooltip text={`${online} Online in this Channel`} position="bottom">
|
<Tooltip text={`${numberFormat(online)} online in this channel`} position="bottom">
|
||||||
{props => (
|
{props => (
|
||||||
<div {...props}>
|
<div {...props}>
|
||||||
<span
|
<span
|
||||||
@ -70,11 +73,11 @@ function MemberCount() {
|
|||||||
marginRight: "0.5em"
|
marginRight: "0.5em"
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span style={{ color: "var(--green-360)" }}>{online}</span>
|
<span style={{ color: "var(--green-360)" }}>{numberFormat(online)}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip text={`${total} Total Server Members`} position="bottom">
|
<Tooltip text={`${numberFormat(total)} total server members`} position="bottom">
|
||||||
{props => (
|
{props => (
|
||||||
<div {...props}>
|
<div {...props}>
|
||||||
<span
|
<span
|
||||||
@ -88,7 +91,7 @@ function MemberCount() {
|
|||||||
marginLeft: "1em"
|
marginLeft: "1em"
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span style={{ color: "var(--primary-400)" }}>{total}</span>
|
<span style={{ color: "var(--primary-400)" }}>{numberFormat(total)}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
import { addAccessory } from "@api/MessageAccessories";
|
import { addAccessory } from "@api/MessageAccessories";
|
||||||
import { definePluginSettings } from "@api/Settings";
|
import { definePluginSettings } from "@api/Settings";
|
||||||
|
import { getSettingStoreLazy } from "@api/SettingsStore";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Devs } from "@utils/constants.js";
|
import { Devs } from "@utils/constants.js";
|
||||||
import { classes } from "@utils/misc";
|
import { classes } from "@utils/misc";
|
||||||
@ -318,9 +319,10 @@ function ChannelMessageEmbedAccessory({ message, channel, guildID }: MessageEmbe
|
|||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const compactModeEnabled = getSettingStoreLazy<boolean>("textAndImages", "messageDisplayCompact")!;
|
||||||
|
|
||||||
function AutomodEmbedAccessory(props: MessageEmbedProps): JSX.Element | null {
|
function AutomodEmbedAccessory(props: MessageEmbedProps): JSX.Element | null {
|
||||||
const { message, channel, guildID } = props;
|
const { message, channel, guildID } = props;
|
||||||
|
|
||||||
const isDM = guildID === "@me";
|
const isDM = guildID === "@me";
|
||||||
const images = getImages(message);
|
const images = getImages(message);
|
||||||
const { parse } = Parser;
|
const { parse } = Parser;
|
||||||
@ -336,7 +338,7 @@ function AutomodEmbedAccessory(props: MessageEmbedProps): JSX.Element | null {
|
|||||||
<span>{isDM ? " - Direct Message" : " - " + GuildStore.getGuild(channel.guild_id)?.name}</span>
|
<span>{isDM ? " - Direct Message" : " - " + GuildStore.getGuild(channel.guild_id)?.name}</span>
|
||||||
</Text>
|
</Text>
|
||||||
}
|
}
|
||||||
compact={false}
|
compact={compactModeEnabled.getSetting()}
|
||||||
content={
|
content={
|
||||||
<>
|
<>
|
||||||
{message.content || message.attachments.length <= images.length
|
{message.content || message.attachments.length <= images.length
|
||||||
@ -363,7 +365,7 @@ export default definePlugin({
|
|||||||
name: "MessageLinkEmbeds",
|
name: "MessageLinkEmbeds",
|
||||||
description: "Adds a preview to messages that link another message",
|
description: "Adds a preview to messages that link another message",
|
||||||
authors: [Devs.TheSun, Devs.Ven, Devs.RyanCaoDev],
|
authors: [Devs.TheSun, Devs.Ven, Devs.RyanCaoDev],
|
||||||
dependencies: ["MessageAccessoriesAPI"],
|
dependencies: ["MessageAccessoriesAPI", "SettingsStoreAPI"],
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: ".embedCard",
|
find: ".embedCard",
|
||||||
|
@ -169,21 +169,14 @@ export default definePlugin({
|
|||||||
try {
|
try {
|
||||||
if (cache == null || (!isBulk && !cache.has(data.id))) return cache;
|
if (cache == null || (!isBulk && !cache.has(data.id))) return cache;
|
||||||
|
|
||||||
const { ignoreBots, ignoreSelf, ignoreUsers, ignoreChannels, ignoreGuilds } = Settings.plugins.MessageLogger;
|
const mutate = (id: string) => {
|
||||||
const myId = UserStore.getCurrentUser().id;
|
|
||||||
|
|
||||||
function mutate(id: string) {
|
|
||||||
const msg = cache.get(id);
|
const msg = cache.get(id);
|
||||||
if (!msg) return;
|
if (!msg) return;
|
||||||
|
|
||||||
const EPHEMERAL = 64;
|
const EPHEMERAL = 64;
|
||||||
const shouldIgnore = data.mlDeleted ||
|
const shouldIgnore = data.mlDeleted ||
|
||||||
(msg.flags & EPHEMERAL) === EPHEMERAL ||
|
(msg.flags & EPHEMERAL) === EPHEMERAL ||
|
||||||
ignoreBots && msg.author?.bot ||
|
this.shouldIgnore(msg);
|
||||||
ignoreSelf && msg.author?.id === myId ||
|
|
||||||
ignoreUsers.includes(msg.author?.id) ||
|
|
||||||
ignoreChannels.includes(msg.channel_id) ||
|
|
||||||
ignoreGuilds.includes(ChannelStore.getChannel(msg.channel_id)?.guild_id);
|
|
||||||
|
|
||||||
if (shouldIgnore) {
|
if (shouldIgnore) {
|
||||||
cache = cache.remove(id);
|
cache = cache.remove(id);
|
||||||
@ -192,7 +185,7 @@ export default definePlugin({
|
|||||||
.set("deleted", true)
|
.set("deleted", true)
|
||||||
.set("attachments", m.attachments.map(a => (a.deleted = true, a))));
|
.set("attachments", m.attachments.map(a => (a.deleted = true, a))));
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
if (isBulk) {
|
if (isBulk) {
|
||||||
data.ids.forEach(mutate);
|
data.ids.forEach(mutate);
|
||||||
@ -205,6 +198,18 @@ export default definePlugin({
|
|||||||
return cache;
|
return cache;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
shouldIgnore(message: any) {
|
||||||
|
const { ignoreBots, ignoreSelf, ignoreUsers, ignoreChannels, ignoreGuilds } = Settings.plugins.MessageLogger;
|
||||||
|
const myId = UserStore.getCurrentUser().id;
|
||||||
|
|
||||||
|
return ignoreBots && message.author?.bot ||
|
||||||
|
ignoreSelf && message.author?.id === myId ||
|
||||||
|
ignoreUsers.includes(message.author?.id) ||
|
||||||
|
ignoreChannels.includes(message.channel_id) ||
|
||||||
|
ignoreChannels.includes(ChannelStore.getChannel(message.channel_id)?.parent_id) ||
|
||||||
|
ignoreGuilds.includes(ChannelStore.getChannel(message.channel_id)?.guild_id);
|
||||||
|
},
|
||||||
|
|
||||||
// Based on canary 9ab8626bcebceaea6da570b9c586172d02b9c996
|
// Based on canary 9ab8626bcebceaea6da570b9c586172d02b9c996
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
@ -237,7 +242,7 @@ export default definePlugin({
|
|||||||
match: /(MESSAGE_UPDATE:function\((\w)\).+?)\.update\((\w)/,
|
match: /(MESSAGE_UPDATE:function\((\w)\).+?)\.update\((\w)/,
|
||||||
replace: "$1" +
|
replace: "$1" +
|
||||||
".update($3,m =>" +
|
".update($3,m =>" +
|
||||||
" (($2.message.flags & 64) === 64 || (Vencord.Settings.plugins.MessageLogger.ignoreBots && $2.message.author?.bot) || (Vencord.Settings.plugins.MessageLogger.ignoreSelf && $2.message.author?.id === Vencord.Webpack.Common.UserStore.getCurrentUser().id)) ? m :" +
|
" (($2.message.flags & 64) === 64 || $self.shouldIgnore($2.message)) ? m :" +
|
||||||
" $2.message.content !== m.editHistory?.[0]?.content && $2.message.content !== m.content ?" +
|
" $2.message.content !== m.editHistory?.[0]?.content && $2.message.content !== m.content ?" +
|
||||||
" m.set('editHistory',[...(m.editHistory || []), $self.makeEdit($2.message, m)]) :" +
|
" m.set('editHistory',[...(m.editHistory || []), $self.makeEdit($2.message, m)]) :" +
|
||||||
" m" +
|
" m" +
|
||||||
@ -321,14 +326,14 @@ export default definePlugin({
|
|||||||
{
|
{
|
||||||
// Attachment renderer
|
// Attachment renderer
|
||||||
// Module 96063
|
// Module 96063
|
||||||
find: "[\"className\",\"attachment\",\"inlineMedia\"",
|
find: "().removeAttachmentHoverButton",
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /((\w)\.className,\w=\2\.attachment),/,
|
match: /((\w)\.className,\w=\2\.attachment),/,
|
||||||
replace: "$1,deleted=$2.attachment?.deleted,"
|
replace: "$1,deleted=$2.attachment?.deleted,"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /\["className","attachment","inlineMedia".+?className:/,
|
match: /\["className","attachment".+?className:/,
|
||||||
replace: "$& (deleted ? 'messagelogger-deleted-attachment ' : '') +"
|
replace: "$& (deleted ? 'messagelogger-deleted-attachment ' : '') +"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -54,6 +54,8 @@ interface IVoiceChannelEffectSendEvent {
|
|||||||
const MOYAI = "🗿";
|
const MOYAI = "🗿";
|
||||||
const MOYAI_URL =
|
const MOYAI_URL =
|
||||||
"https://raw.githubusercontent.com/MeguminSama/VencordPlugins/main/plugins/moyai/moyai.mp3";
|
"https://raw.githubusercontent.com/MeguminSama/VencordPlugins/main/plugins/moyai/moyai.mp3";
|
||||||
|
const MOYAI_URL_HD =
|
||||||
|
"https://raw.githubusercontent.com/MeguminSama/VencordPlugins/main/plugins/moyai/moyai_hd.wav";
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
const settings = definePluginSettings({
|
||||||
volume: {
|
volume: {
|
||||||
@ -63,6 +65,14 @@ const settings = definePluginSettings({
|
|||||||
default: 0.5,
|
default: 0.5,
|
||||||
stickToMarkers: false
|
stickToMarkers: false
|
||||||
},
|
},
|
||||||
|
quality: {
|
||||||
|
description: "Quality of the 🗿🗿🗿",
|
||||||
|
type: OptionType.SELECT,
|
||||||
|
options: [
|
||||||
|
{ label: "Normal", value: "Normal", default: true },
|
||||||
|
{ label: "HD", value: "HD" }
|
||||||
|
],
|
||||||
|
},
|
||||||
triggerWhenUnfocused: {
|
triggerWhenUnfocused: {
|
||||||
description: "Trigger the 🗿 even when the window is unfocused",
|
description: "Trigger the 🗿 even when the window is unfocused",
|
||||||
type: OptionType.BOOLEAN,
|
type: OptionType.BOOLEAN,
|
||||||
@ -157,7 +167,11 @@ function getMoyaiCount(message: string) {
|
|||||||
function boom() {
|
function boom() {
|
||||||
if (!settings.store.triggerWhenUnfocused && !document.hasFocus()) return;
|
if (!settings.store.triggerWhenUnfocused && !document.hasFocus()) return;
|
||||||
const audioElement = document.createElement("audio");
|
const audioElement = document.createElement("audio");
|
||||||
audioElement.src = MOYAI_URL;
|
|
||||||
|
audioElement.src = settings.store.quality === "HD"
|
||||||
|
? MOYAI_URL_HD
|
||||||
|
: MOYAI_URL;
|
||||||
|
|
||||||
audioElement.volume = settings.store.volume;
|
audioElement.volume = settings.store.volume;
|
||||||
audioElement.play();
|
audioElement.play();
|
||||||
}
|
}
|
||||||
|
@ -55,8 +55,8 @@ export default definePlugin({
|
|||||||
{
|
{
|
||||||
find: '"MaskedLinkStore"',
|
find: '"MaskedLinkStore"',
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /return ((\i)\.apply\(this,arguments\))(?=\}function \i.{0,200}\.trusted)/,
|
match: /return ((\i)\.apply\(this,arguments\))(?=\}function \i.{0,250}\.trusted)/,
|
||||||
replace: "return $self.handleLink(...arguments).then(handled => handled || $1)"
|
replace: "return $self.handleLink(...arguments).then(handled => handled||$1)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Make Spotify profile activity links open in app on web
|
// Make Spotify profile activity links open in app on web
|
||||||
|
@ -178,12 +178,12 @@ export default definePlugin({
|
|||||||
start() {
|
start() {
|
||||||
addContextMenuPatch("user-context", this.userContextMenuPatch);
|
addContextMenuPatch("user-context", this.userContextMenuPatch);
|
||||||
addContextMenuPatch("channel-context", this.channelContextMenuPatch);
|
addContextMenuPatch("channel-context", this.channelContextMenuPatch);
|
||||||
addContextMenuPatch("guild-context", this.guildContextMenuPatch);
|
addContextMenuPatch(["guild-context", "guild-header-popout"], this.guildContextMenuPatch);
|
||||||
},
|
},
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
removeContextMenuPatch("user-context", this.userContextMenuPatch);
|
removeContextMenuPatch("user-context", this.userContextMenuPatch);
|
||||||
removeContextMenuPatch("channel-context", this.channelContextMenuPatch);
|
removeContextMenuPatch("channel-context", this.channelContextMenuPatch);
|
||||||
removeContextMenuPatch("guild-context", this.guildContextMenuPatch);
|
removeContextMenuPatch(["guild-context", "guild-header-popout"], this.guildContextMenuPatch);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
83
src/plugins/pictureInPicture.tsx
Normal file
83
src/plugins/pictureInPicture.tsx
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { definePluginSettings } from "@api/Settings";
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
|
import { React, Tooltip } from "@webpack/common";
|
||||||
|
|
||||||
|
const settings = definePluginSettings({
|
||||||
|
loop: {
|
||||||
|
description: "Whether to make the PiP video loop or not",
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
default: true,
|
||||||
|
restartNeeded: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "PictureInPicture",
|
||||||
|
description: "Adds picture in picture to videos (next to the Download button)",
|
||||||
|
authors: [Devs.Lumap],
|
||||||
|
settings,
|
||||||
|
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: ".onRemoveAttachment,",
|
||||||
|
replacement: {
|
||||||
|
match: /\.nonMediaAttachment,!(\i).{0,7}children:\[(\i),/,
|
||||||
|
replace: "$&$1&&$2&&$self.renderPiPButton(),"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
renderPiPButton: ErrorBoundary.wrap(() => {
|
||||||
|
return (
|
||||||
|
<Tooltip text="Toggle Picture in Picture">
|
||||||
|
{tooltipProps => (
|
||||||
|
<div
|
||||||
|
{...tooltipProps}
|
||||||
|
role="button"
|
||||||
|
style={{
|
||||||
|
cursor: "pointer",
|
||||||
|
paddingTop: "4px",
|
||||||
|
paddingLeft: "4px",
|
||||||
|
paddingRight: "4px",
|
||||||
|
}}
|
||||||
|
onClick={e => {
|
||||||
|
const video = e.currentTarget.parentNode!.parentNode!.querySelector("video")!;
|
||||||
|
const videoClone = document.body.appendChild(video.cloneNode(true)) as HTMLVideoElement;
|
||||||
|
|
||||||
|
videoClone.loop = settings.store.loop;
|
||||||
|
videoClone.style.display = "none";
|
||||||
|
videoClone.onleavepictureinpicture = () => videoClone.remove();
|
||||||
|
|
||||||
|
function launchPiP() {
|
||||||
|
videoClone.currentTime = video.currentTime;
|
||||||
|
videoClone.requestPictureInPicture();
|
||||||
|
video.pause();
|
||||||
|
videoClone.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (videoClone.readyState === 4 /* HAVE_ENOUGH_DATA */)
|
||||||
|
launchPiP();
|
||||||
|
else
|
||||||
|
videoClone.onloadedmetadata = launchPiP;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg width="24px" height="24px" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="var(--interactive-normal)"
|
||||||
|
d="M21 3a1 1 0 0 1 1 1v7h-2V5H4v14h6v2H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h18zm0 10a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1h-8a1 1 0 0 1-1-1v-6a1 1 0 0 1 1-1h8zm-1 2h-6v4h6v-4z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}, { noop: true })
|
||||||
|
});
|
@ -122,6 +122,14 @@ export default definePlugin({
|
|||||||
// ....concat(pins).concat(toArray(channelIds).filter(c => !isPinned(c)))
|
// ....concat(pins).concat(toArray(channelIds).filter(c => !isPinned(c)))
|
||||||
replace: ".concat($self.getSnapshot()).concat($2.filter(c=>!$self.isPinned(c)))"
|
replace: ".concat($self.getSnapshot()).concat($2.filter(c=>!$self.isPinned(c)))"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
// fix alt+shift+up/down
|
||||||
|
{
|
||||||
|
find: '"alt+shift+down"',
|
||||||
|
replacement: {
|
||||||
|
match: /(?<=return \i===\i\.ME\?)\i\.\i\.getPrivateChannelIds\(\)/,
|
||||||
|
replace: "$self.getSnapshot().concat($&.filter(c=>!$self.isPinned(c)))"
|
||||||
|
}
|
||||||
|
},
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
5
src/plugins/previewMessage/README.md
Normal file
5
src/plugins/previewMessage/README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# PreviewMessage
|
||||||
|
|
||||||
|
Lets you preview your message before sending it.
|
||||||
|
|
||||||
|
![Example](https://i.imgur.com/etqbkzu.png)
|
@ -16,38 +16,96 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { sendBotMessage } from "@api/Commands";
|
import { generateId, sendBotMessage } from "@api/Commands";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin from "@utils/types";
|
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 {
|
interface Props {
|
||||||
type: {
|
type: {
|
||||||
analyticsName: string;
|
analyticsName: string;
|
||||||
|
isEmpty: boolean;
|
||||||
|
attachments: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const UploadStore = findByPropsLazy("getUploads");
|
||||||
|
|
||||||
const getDraft = (channelId: string) => DraftStore.getDraft(channelId, DraftType.ChannelMessage);
|
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 (
|
return (
|
||||||
<Tooltip text="Preview Message">
|
<Tooltip text="Preview Message">
|
||||||
{tooltipProps => (
|
{tooltipProps => (
|
||||||
<Button
|
<Button
|
||||||
{...tooltipProps}
|
{...tooltipProps}
|
||||||
onClick={() =>
|
onClick={async () =>
|
||||||
sendBotMessage(
|
sendBotMessage(
|
||||||
channelId,
|
channelId,
|
||||||
{
|
{
|
||||||
content: getDraft(channelId),
|
content: getDraft(channelId),
|
||||||
author: UserStore.getCurrentUser()
|
author: UserStore.getCurrentUser(),
|
||||||
|
attachments: hasAttachments ? await getAttachments(channelId) : undefined,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
size=""
|
size=""
|
||||||
@ -67,7 +125,7 @@ export function PreviewButton(chatBoxProps: Props) {
|
|||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "PreviewMessage",
|
name: "PreviewMessage",
|
||||||
description: "Lets you preview your message before sending it",
|
description: "Lets you preview your message before sending it.",
|
||||||
authors: [Devs.Aria],
|
authors: [Devs.Aria],
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
@ -21,14 +21,11 @@ import { VENCORD_USER_AGENT } from "@utils/constants";
|
|||||||
import { debounce } from "@utils/debounce";
|
import { debounce } from "@utils/debounce";
|
||||||
import { getCurrentChannel } from "@utils/discord";
|
import { getCurrentChannel } from "@utils/discord";
|
||||||
import { useAwaiter } from "@utils/react";
|
import { useAwaiter } from "@utils/react";
|
||||||
import { findStoreLazy } from "@webpack";
|
import { UserProfileStore, UserStore } from "@webpack/common";
|
||||||
import { UserStore } from "@webpack/common";
|
|
||||||
|
|
||||||
import { settings } from "./settings";
|
import { settings } from "./settings";
|
||||||
import { PronounCode, PronounMapping, PronounsResponse } from "./types";
|
import { PronounCode, PronounMapping, PronounsResponse } from "./types";
|
||||||
|
|
||||||
const UserProfileStore = findStoreLazy("UserProfileStore");
|
|
||||||
|
|
||||||
type PronounsWithSource = [string | null, string];
|
type PronounsWithSource = [string | null, string];
|
||||||
const EmptyPronouns: PronounsWithSource = [null, ""];
|
const EmptyPronouns: PronounsWithSource = [null, ""];
|
||||||
|
|
||||||
|
@ -145,7 +145,7 @@ export default definePlugin({
|
|||||||
find: "window.webkitAudioContext",
|
find: "window.webkitAudioContext",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<=\i\.acquire=function\((\i)\)\{return )navigator\.mediaDevices\.getUserMedia\(\1\)(?=\})/,
|
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,
|
setEnabled,
|
||||||
isEnabled: () => settings.store.isEnabled,
|
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;
|
if (!settings.store.isEnabled) return stream;
|
||||||
|
|
||||||
const audioCtx = new AudioContext();
|
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);
|
||||||
|
}
|
@ -27,13 +27,12 @@ import { copyWithToast } from "@utils/misc";
|
|||||||
import { LazyComponent } from "@utils/react";
|
import { LazyComponent } from "@utils/react";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { findByCode, findByCodeLazy, findByPropsLazy, findStoreLazy } from "@webpack";
|
import { findByCode, findByCodeLazy, findByPropsLazy, findStoreLazy } from "@webpack";
|
||||||
import { Text, Tooltip } from "@webpack/common";
|
import { Text, Tooltip, UserProfileStore } from "@webpack/common";
|
||||||
import { User } from "discord-types/general";
|
import { User } from "discord-types/general";
|
||||||
|
|
||||||
import { VerifiedIcon } from "./VerifiedIcon";
|
import { VerifiedIcon } from "./VerifiedIcon";
|
||||||
|
|
||||||
const Section = LazyComponent(() => findByCode("().lastSection"));
|
const Section = LazyComponent(() => findByCode("().lastSection"));
|
||||||
const UserProfileStore = findStoreLazy("UserProfileStore");
|
|
||||||
const ThemeStore = findStoreLazy("ThemeStore");
|
const ThemeStore = findStoreLazy("ThemeStore");
|
||||||
const platforms: { get(type: string): ConnectionPlatform; } = findByPropsLazy("isSupported", "getByUrl");
|
const platforms: { get(type: string): ConnectionPlatform; } = findByPropsLazy("isSupported", "getByUrl");
|
||||||
const getTheme: (user: User, displayProfile: any) => any = findByCodeLazy(',"--profile-gradient-primary-color"');
|
const getTheme: (user: User, displayProfile: any) => any = findByCodeLazy(',"--profile-gradient-primary-color"');
|
||||||
|
@ -21,13 +21,14 @@ import "./styles.css";
|
|||||||
import { definePluginSettings } from "@api/Settings";
|
import { definePluginSettings } from "@api/Settings";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { Message } from "discord-types/general";
|
import { Message, User } from "discord-types/general";
|
||||||
|
|
||||||
interface UsernameProps {
|
interface UsernameProps {
|
||||||
author: { nick: string; };
|
author: { nick: string; };
|
||||||
message: Message;
|
message: Message;
|
||||||
withMentionPrefix?: boolean;
|
withMentionPrefix?: boolean;
|
||||||
isRepliedMessage: boolean;
|
isRepliedMessage: boolean;
|
||||||
|
userOverride?: User;
|
||||||
}
|
}
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
const settings = definePluginSettings({
|
||||||
@ -67,11 +68,12 @@ export default definePlugin({
|
|||||||
],
|
],
|
||||||
settings,
|
settings,
|
||||||
|
|
||||||
renderUsername: ({ author, message, isRepliedMessage, withMentionPrefix }: UsernameProps) => {
|
renderUsername: ({ author, message, isRepliedMessage, withMentionPrefix, userOverride }: UsernameProps) => {
|
||||||
try {
|
try {
|
||||||
let { username } = message.author;
|
const user = userOverride ?? message.author;
|
||||||
|
let { username } = user;
|
||||||
if (settings.store.displayNames)
|
if (settings.store.displayNames)
|
||||||
username = (message.author as any).globalName || username;
|
username = (user as any).globalName || username;
|
||||||
|
|
||||||
const { nick } = author;
|
const { nick } = author;
|
||||||
const prefix = withMentionPrefix ? "@" : "";
|
const prefix = withMentionPrefix ? "@" : "";
|
||||||
|
35
src/plugins/showTimeouts.ts
Normal file
35
src/plugins/showTimeouts.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import definePlugin from "@utils/types";
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "ShowTimeouts",
|
||||||
|
description: "Display member timeout icons in chat regardless of permissions.",
|
||||||
|
authors: [Devs.Dolfies],
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: "showCommunicationDisabledStyles",
|
||||||
|
replacement: {
|
||||||
|
match: /&&\i\.\i\.canManageUser\(\i\.\i\.MODERATE_MEMBERS,\i\.author,\i\)/,
|
||||||
|
replace: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
23
src/plugins/startupTimings/StartupTimingPage.css
Normal file
23
src/plugins/startupTimings/StartupTimingPage.css
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
.vc-startuptimings-server-trace {
|
||||||
|
color: var(--header-primary);
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-startuptimings-grid {
|
||||||
|
color: var(--header-primary);
|
||||||
|
display: grid;
|
||||||
|
gap: 2px 10px;
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-startuptimings-4-cols {
|
||||||
|
grid-template-columns: repeat(3, auto) 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-startuptimings-3-cols {
|
||||||
|
grid-template-columns: repeat(2, auto) 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-startuptimings-2-cols {
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
}
|
@ -16,11 +16,35 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import "./StartupTimingPage.css";
|
||||||
|
|
||||||
|
import { classNameFactory } from "@api/Styles";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Flex } from "@components/Flex";
|
import { Flex } from "@components/Flex";
|
||||||
|
import { Margins } from "@utils/margins";
|
||||||
|
import { classes } from "@utils/misc";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
import { Forms, React } from "@webpack/common";
|
import { Forms, React } from "@webpack/common";
|
||||||
|
|
||||||
|
|
||||||
|
export const cl = classNameFactory("vc-startuptimings-");
|
||||||
|
|
||||||
|
interface ITTITrackerEvent {
|
||||||
|
emoji: string;
|
||||||
|
name: string;
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
hasData(): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ITTITracker {
|
||||||
|
serializeTTITracker(): Record<string, string | number | boolean | null | undefined>;
|
||||||
|
[event: string]: ITTITrackerEvent | string | boolean | null | any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Time-To-Interactive Tracker */
|
||||||
|
const TTITracker: ITTITracker = findByPropsLazy("serializeTTITracker");
|
||||||
|
|
||||||
interface AppStartPerformance {
|
interface AppStartPerformance {
|
||||||
prefix: string;
|
prefix: string;
|
||||||
logs: Log[];
|
logs: Log[];
|
||||||
@ -91,11 +115,11 @@ function TimingSection({ title, logs, traceEnd }: TimingSectionProps) {
|
|||||||
<Forms.FormSection title={title} tag="h1">
|
<Forms.FormSection title={title} tag="h1">
|
||||||
<code>
|
<code>
|
||||||
{traceEnd && (
|
{traceEnd && (
|
||||||
<div style={{ color: "var(--header-primary)", marginBottom: 5, userSelect: "text" }}>
|
<div className={cl("server-trace")} style={{ marginBottom: 5 }}>
|
||||||
Trace ended at: {(new Date(traceEnd)).toTimeString()}
|
Trace ended at: {(new Date(traceEnd)).toTimeString()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div style={{ color: "var(--header-primary)", display: "grid", gridTemplateColumns: "repeat(3, auto) 1fr", gap: "2px 10px", userSelect: "text" }}>
|
<div className={classes(cl("grid"), cl("4-cols"))}>
|
||||||
<span>Start</span>
|
<span>Start</span>
|
||||||
<span>Interval</span>
|
<span>Interval</span>
|
||||||
<span>Delta</span>
|
<span>Delta</span>
|
||||||
@ -119,13 +143,66 @@ function ServerTrace({ trace }: ServerTraceProps) {
|
|||||||
return (
|
return (
|
||||||
<Forms.FormSection title="Server Trace" tag="h2">
|
<Forms.FormSection title="Server Trace" tag="h2">
|
||||||
<code>
|
<code>
|
||||||
<Flex flexDirection="column" style={{ color: "var(--header-primary)", gap: 5, userSelect: "text" }}>
|
<Flex flexDirection="column" className={cl("server-trace")} style={{ gap: 5 }}>
|
||||||
{lines.map(line => (
|
{lines.map(line => (
|
||||||
<span>{line}</span>
|
<span>{line}</span>
|
||||||
))}
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
</code>
|
</code>
|
||||||
</Forms.FormSection>
|
</Forms.FormSection >
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TTIAnalytics() {
|
||||||
|
const analytics = TTITracker.serializeTTITracker();
|
||||||
|
const filteredAnalytics = Object.entries(analytics).filter(([key, value]) => value != null && !/_start$|_end$/.test(key));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorBoundary>
|
||||||
|
<Forms.FormSection title="TTI Analytics" tag="h2">
|
||||||
|
<code>
|
||||||
|
<div className={classes(cl("grid"), cl("2-cols"))}>
|
||||||
|
{filteredAnalytics.map(([key, value]) => (
|
||||||
|
<React.Fragment>
|
||||||
|
<span><pre>{key}</pre></span>
|
||||||
|
<span><pre>{`${value}`}</pre></span>
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</code>
|
||||||
|
</Forms.FormSection>
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TTITimingsProps {
|
||||||
|
records: [string, ITTITrackerEvent][];
|
||||||
|
title: string;
|
||||||
|
type: "registered" | "unregistered";
|
||||||
|
}
|
||||||
|
|
||||||
|
function TTITimings({ records, title, type }: TTITimingsProps) {
|
||||||
|
const isRegistered = type === "registered";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorBoundary>
|
||||||
|
<Forms.FormSection title={title} tag="h2">
|
||||||
|
<code>
|
||||||
|
<div className={classes(cl("grid"), cl(isRegistered ? "3-cols" : "2-cols"))}>
|
||||||
|
{isRegistered && <span>Duration</span>}
|
||||||
|
<span>Key</span>
|
||||||
|
<span style={{ marginBottom: 5 }}>Event</span>
|
||||||
|
{records.map(([key, event]) => (
|
||||||
|
<React.Fragment key={key}>
|
||||||
|
{isRegistered && <span><pre>{event.end - event.start}ms</pre></span>}
|
||||||
|
<span><pre>{key}</pre></span>
|
||||||
|
<span><pre>{event.emoji} {event.name}</pre></span>
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</code>
|
||||||
|
</Forms.FormSection>
|
||||||
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,6 +211,12 @@ function StartupTimingPage() {
|
|||||||
|
|
||||||
const serverTrace = AppStartPerformance.logGroups.find(g => g.serverTrace)?.serverTrace;
|
const serverTrace = AppStartPerformance.logGroups.find(g => g.serverTrace)?.serverTrace;
|
||||||
|
|
||||||
|
const registeredTTITimings: [string, ITTITrackerEvent][] = (Object.entries(TTITracker))
|
||||||
|
.filter(([, value]) => value?.hasData?.());
|
||||||
|
|
||||||
|
const unregisteredTTITimings: [string, ITTITrackerEvent][] = (Object.entries(TTITracker))
|
||||||
|
.filter(([, value]) => value?.hasData && !value.hasData());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<TimingSection
|
<TimingSection
|
||||||
@ -141,9 +224,26 @@ function StartupTimingPage() {
|
|||||||
logs={AppStartPerformance.logs}
|
logs={AppStartPerformance.logs}
|
||||||
traceEnd={AppStartPerformance.endTime_}
|
traceEnd={AppStartPerformance.endTime_}
|
||||||
/>
|
/>
|
||||||
{/* Lazy Divider */}
|
<Forms.FormDivider className={classes(Margins.top16, Margins.bottom16)} />
|
||||||
<div style={{ marginTop: 5 }}> </div>
|
|
||||||
{serverTrace && <ServerTrace trace={serverTrace} />}
|
{serverTrace && <ServerTrace trace={serverTrace} />}
|
||||||
|
<Forms.FormDivider className={classes(Margins.top16, Margins.bottom16)} />
|
||||||
|
|
||||||
|
<TTIAnalytics />
|
||||||
|
<Forms.FormDivider className={classes(Margins.top16, Margins.bottom16)} />
|
||||||
|
|
||||||
|
<TTITimings
|
||||||
|
title="Registered TTI Timings"
|
||||||
|
records={registeredTTITimings}
|
||||||
|
type="registered"
|
||||||
|
/>
|
||||||
|
<Forms.FormDivider className={classes(Margins.top16, Margins.bottom16)} />
|
||||||
|
|
||||||
|
<TTITimings
|
||||||
|
title="Unregistered TTI Timings"
|
||||||
|
records={unregisteredTTITimings}
|
||||||
|
type="unregistered"
|
||||||
|
/>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -48,7 +48,7 @@ export default definePlugin({
|
|||||||
name: "vencord-debug",
|
name: "vencord-debug",
|
||||||
description: "Send Vencord Debug info",
|
description: "Send Vencord Debug info",
|
||||||
predicate: ctx => AllowedChannelIds.includes(ctx.channel.id),
|
predicate: ctx => AllowedChannelIds.includes(ctx.channel.id),
|
||||||
execute() {
|
async execute() {
|
||||||
const { RELEASE_CHANNEL } = window.GLOBAL_ENV;
|
const { RELEASE_CHANNEL } = window.GLOBAL_ENV;
|
||||||
|
|
||||||
const client = (() => {
|
const client = (() => {
|
||||||
@ -75,6 +75,10 @@ export default definePlugin({
|
|||||||
OpenAsar: "openasar" in window,
|
OpenAsar: "openasar" in window,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (IS_DISCORD_DESKTOP) {
|
||||||
|
info["Last Crash Reason"] = (await DiscordNative.processUtils.getLastCrash())?.rendererCrashReason ?? "N/A";
|
||||||
|
}
|
||||||
|
|
||||||
const debugInfo = `
|
const debugInfo = `
|
||||||
**Vencord Debug Info**
|
**Vencord Debug Info**
|
||||||
>>> ${Object.entries(info).map(([k, v]) => `${k}: ${v}`).join("\n")}
|
>>> ${Object.entries(info).map(([k, v]) => `${k}: ${v}`).join("\n")}
|
||||||
|
20
src/plugins/themeAttributes/README.md
Normal file
20
src/plugins/themeAttributes/README.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# ThemeAttributes
|
||||||
|
|
||||||
|
This plugin adds data attributes to various elements inside Discord
|
||||||
|
|
||||||
|
This allows themes to more easily theme those elements or even do things that otherwise wouldn't be possible
|
||||||
|
|
||||||
|
## Available Attributes
|
||||||
|
|
||||||
|
### All Tab Bars (User Settings, Server Settings, etc)
|
||||||
|
|
||||||
|
`data-tab-id` contains the id of that tab
|
||||||
|
|
||||||
|
![image](https://github.com/Vendicated/Vencord/assets/45497981/1263b782-f673-4f09-820c-4cc366d062ad)
|
||||||
|
|
||||||
|
### Chat Messages
|
||||||
|
|
||||||
|
- `data-author-id` contains the id of the author
|
||||||
|
- `data-is-self` is a boolean indicating whether this is the current user's message
|
||||||
|
|
||||||
|
![image](https://github.com/Vendicated/Vencord/assets/45497981/34bd5053-3381-402f-82b2-9c812cc7e122)
|
45
src/plugins/themeAttributes/index.ts
Normal file
45
src/plugins/themeAttributes/index.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import definePlugin from "@utils/types";
|
||||||
|
import { UserStore } from "@webpack/common";
|
||||||
|
import { Message } from "discord-types/general";
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "ThemeAttributes",
|
||||||
|
description: "Adds data attributes to various elements for theming purposes",
|
||||||
|
authors: [Devs.Ven],
|
||||||
|
|
||||||
|
patches: [
|
||||||
|
// Add data-tab-id to all tab bar items
|
||||||
|
// This for examples applies to the User and Server settings sidebars
|
||||||
|
{
|
||||||
|
find: ".tabBarRef",
|
||||||
|
replacement: {
|
||||||
|
match: /style:this\.getStyle\(\),role:"tab"/,
|
||||||
|
replace: "$&,'data-tab-id':this.props.id"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Add data-author-id and data-is-self to all messages
|
||||||
|
{
|
||||||
|
find: ".messageListItem",
|
||||||
|
replacement: {
|
||||||
|
match: /\.messageListItem(?=,"aria)/,
|
||||||
|
replace: "$&,...$self.getMessageProps(arguments[0])"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
getMessageProps(props: { message: Message; }) {
|
||||||
|
const authorId = props.message?.author?.id;
|
||||||
|
return {
|
||||||
|
"data-author-id": authorId,
|
||||||
|
"data-is-self": authorId && authorId === UserStore.getCurrentUser()?.id
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
@ -80,6 +80,9 @@ export default definePlugin({
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
||||||
|
data,
|
||||||
|
|
||||||
settingsAboutComponent: () => {
|
settingsAboutComponent: () => {
|
||||||
return (
|
return (
|
||||||
<Link href="https://github.com/AutumnVN/usrbg#how-to-request-your-own-usrbg-banner">CLICK HERE TO GET YOUR OWN BANNER</Link>
|
<Link href="https://github.com/AutumnVN/usrbg#how-to-request-your-own-usrbg-banner">CLICK HERE TO GET YOUR OWN BANNER</Link>
|
||||||
@ -116,7 +119,9 @@ export default definePlugin({
|
|||||||
enableStyle(style);
|
enableStyle(style);
|
||||||
|
|
||||||
const res = await fetch(BASE_URL);
|
const res = await fetch(BASE_URL);
|
||||||
if (res.ok)
|
if (res.ok) {
|
||||||
data = await res.json();
|
data = await res.json();
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -16,8 +16,10 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
|
||||||
import { addButton, removeButton } from "@api/MessagePopover";
|
import { addButton, removeButton } from "@api/MessagePopover";
|
||||||
import { definePluginSettings } from "@api/Settings";
|
import { definePluginSettings } from "@api/Settings";
|
||||||
|
import { CodeBlock } from "@components/CodeBlock";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Flex } from "@components/Flex";
|
import { Flex } from "@components/Flex";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
@ -25,12 +27,12 @@ import { Margins } from "@utils/margins";
|
|||||||
import { copyWithToast } from "@utils/misc";
|
import { copyWithToast } from "@utils/misc";
|
||||||
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { Button, ChannelStore, Forms, Parser, Text } from "@webpack/common";
|
import { Button, ChannelStore, Forms, Menu, Text } from "@webpack/common";
|
||||||
import { Message } from "discord-types/general";
|
import { Message } from "discord-types/general";
|
||||||
|
|
||||||
|
|
||||||
const CopyIcon = () => {
|
const CopyIcon = () => {
|
||||||
return <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" width="22" height="22">
|
return <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" width="18" height="18">
|
||||||
<path d="M12.9297 3.25007C12.7343 3.05261 12.4154 3.05226 12.2196 3.24928L11.5746 3.89824C11.3811 4.09297 11.3808 4.40733 11.5739 4.60245L16.5685 9.64824C16.7614 9.84309 16.7614 10.1569 16.5685 10.3517L11.5739 15.3975C11.3808 15.5927 11.3811 15.907 11.5746 16.1017L12.2196 16.7507C12.4154 16.9477 12.7343 16.9474 12.9297 16.7499L19.2604 10.3517C19.4532 10.1568 19.4532 9.84314 19.2604 9.64832L12.9297 3.25007Z" />
|
<path d="M12.9297 3.25007C12.7343 3.05261 12.4154 3.05226 12.2196 3.24928L11.5746 3.89824C11.3811 4.09297 11.3808 4.40733 11.5739 4.60245L16.5685 9.64824C16.7614 9.84309 16.7614 10.1569 16.5685 10.3517L11.5739 15.3975C11.3808 15.5927 11.3811 15.907 11.5746 16.1017L12.2196 16.7507C12.4154 16.9477 12.7343 16.9474 12.9297 16.7499L19.2604 10.3517C19.4532 10.1568 19.4532 9.84314 19.2604 9.64832L12.9297 3.25007Z" />
|
||||||
<path d="M8.42616 4.60245C8.6193 4.40733 8.61898 4.09297 8.42545 3.89824L7.78047 3.24928C7.58466 3.05226 7.26578 3.05261 7.07041 3.25007L0.739669 9.64832C0.5469 9.84314 0.546901 10.1568 0.739669 10.3517L7.07041 16.7499C7.26578 16.9474 7.58465 16.9477 7.78047 16.7507L8.42545 16.1017C8.61898 15.907 8.6193 15.5927 8.42616 15.3975L3.43155 10.3517C3.23869 10.1569 3.23869 9.84309 3.43155 9.64824L8.42616 4.60245Z" />
|
<path d="M8.42616 4.60245C8.6193 4.40733 8.61898 4.09297 8.42545 3.89824L7.78047 3.24928C7.58466 3.05226 7.26578 3.05261 7.07041 3.25007L0.739669 9.64832C0.5469 9.84314 0.546901 10.1568 0.739669 10.3517L7.07041 16.7499C7.26578 16.9474 7.58465 16.9477 7.78047 16.7507L8.42545 16.1017C8.61898 15.907 8.6193 15.5927 8.42616 15.3975L3.43155 10.3517C3.23869 10.1569 3.23869 9.84309 3.43155 9.64824L8.42616 4.60245Z" />
|
||||||
</svg>;
|
</svg>;
|
||||||
@ -58,19 +60,7 @@ function cleanMessage(msg: Message) {
|
|||||||
return clone;
|
return clone;
|
||||||
}
|
}
|
||||||
|
|
||||||
function CodeBlock(props: { content: string, lang: string; }) {
|
function openViewRawModal(json: string, type: string, msgContent?: string) {
|
||||||
return (
|
|
||||||
// make text selectable
|
|
||||||
<div style={{ userSelect: "text" }}>
|
|
||||||
{Parser.defaultRules.codeBlock.react(props, null, {})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function openViewRawModal(msg: Message) {
|
|
||||||
msg = cleanMessage(msg);
|
|
||||||
const msgJson = JSON.stringify(msg, null, 4);
|
|
||||||
|
|
||||||
const key = openModal(props => (
|
const key = openModal(props => (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<ModalRoot {...props} size={ModalSize.LARGE}>
|
<ModalRoot {...props} size={ModalSize.LARGE}>
|
||||||
@ -80,26 +70,28 @@ function openViewRawModal(msg: Message) {
|
|||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<div style={{ padding: "16px 0" }}>
|
<div style={{ padding: "16px 0" }}>
|
||||||
{!!msg.content && (
|
{!!msgContent && (
|
||||||
<>
|
<>
|
||||||
<Forms.FormTitle tag="h5">Content</Forms.FormTitle>
|
<Forms.FormTitle tag="h5">Content</Forms.FormTitle>
|
||||||
<CodeBlock content={msg.content} lang="" />
|
<CodeBlock content={msgContent} lang="" />
|
||||||
<Forms.FormDivider className={Margins.bottom20} />
|
<Forms.FormDivider className={Margins.bottom20} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Forms.FormTitle tag="h5">Message Data</Forms.FormTitle>
|
<Forms.FormTitle tag="h5">{type} Data</Forms.FormTitle>
|
||||||
<CodeBlock content={msgJson} lang="json" />
|
<CodeBlock content={json} lang="json" />
|
||||||
</div>
|
</div>
|
||||||
</ModalContent >
|
</ModalContent >
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Flex cellSpacing={10}>
|
<Flex cellSpacing={10}>
|
||||||
<Button onClick={() => copyWithToast(msgJson, "Message data copied to clipboard!")}>
|
<Button onClick={() => copyWithToast(json, `${type} data copied to clipboard!`)}>
|
||||||
Copy Message JSON
|
Copy {type} JSON
|
||||||
</Button>
|
|
||||||
<Button onClick={() => copyWithToast(msg.content, "Content copied to clipboard!")}>
|
|
||||||
Copy Raw Content
|
|
||||||
</Button>
|
</Button>
|
||||||
|
{!!msgContent && (
|
||||||
|
<Button onClick={() => copyWithToast(msgContent, "Content copied to clipboard!")}>
|
||||||
|
Copy Raw Content
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalRoot >
|
</ModalRoot >
|
||||||
@ -107,6 +99,13 @@ function openViewRawModal(msg: Message) {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openViewRawModalMessage(msg: Message) {
|
||||||
|
msg = cleanMessage(msg);
|
||||||
|
const msgJson = JSON.stringify(msg, null, 4);
|
||||||
|
|
||||||
|
return openViewRawModal(msgJson, "Message", msg.content);
|
||||||
|
}
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
const settings = definePluginSettings({
|
||||||
clickMethod: {
|
clickMethod: {
|
||||||
description: "Change the button to view the raw content/data of any message.",
|
description: "Change the button to view the raw content/data of any message.",
|
||||||
@ -118,10 +117,34 @@ const settings = definePluginSettings({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function MakeContextCallback(name: string) {
|
||||||
|
const callback: NavContextMenuPatchCallback = (children, props) => () => {
|
||||||
|
if (name === "Guild" && !props.guild) return;
|
||||||
|
const lastChild = children.at(-1);
|
||||||
|
if (lastChild?.key === "developer-actions") {
|
||||||
|
const p = lastChild.props;
|
||||||
|
if (!Array.isArray(p.children))
|
||||||
|
p.children = [p.children];
|
||||||
|
({ children } = p);
|
||||||
|
}
|
||||||
|
|
||||||
|
children.splice(-1, 0,
|
||||||
|
<Menu.MenuItem
|
||||||
|
id={`vc-view-${name.toLowerCase()}-raw`}
|
||||||
|
label="View Raw"
|
||||||
|
action={() => openViewRawModal(JSON.stringify(props[name.toLowerCase()], null, 4), name)}
|
||||||
|
icon={CopyIcon}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
return callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "ViewRaw",
|
name: "ViewRaw",
|
||||||
description: "Copy and view the raw content/data of any message.",
|
description: "Copy and view the raw content/data of any message, channel or guild",
|
||||||
authors: [Devs.KingFish, Devs.Ven, Devs.rad],
|
authors: [Devs.KingFish, Devs.Ven, Devs.rad, Devs.ImLvna],
|
||||||
dependencies: ["MessagePopoverAPI"],
|
dependencies: ["MessagePopoverAPI"],
|
||||||
settings,
|
settings,
|
||||||
|
|
||||||
@ -131,7 +154,7 @@ export default definePlugin({
|
|||||||
if (settings.store.clickMethod === "Right") {
|
if (settings.store.clickMethod === "Right") {
|
||||||
copyWithToast(msg.content);
|
copyWithToast(msg.content);
|
||||||
} else {
|
} else {
|
||||||
openViewRawModal(msg);
|
openViewRawModalMessage(msg);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -143,7 +166,7 @@ export default definePlugin({
|
|||||||
} else {
|
} else {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
openViewRawModal(msg);
|
openViewRawModalMessage(msg);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -160,9 +183,16 @@ export default definePlugin({
|
|||||||
onContextMenu: handleContextMenu
|
onContextMenu: handleContextMenu
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addContextMenuPatch("guild-context", MakeContextCallback("Guild"));
|
||||||
|
addContextMenuPatch("channel-context", MakeContextCallback("Channel"));
|
||||||
|
addContextMenuPatch("user-context", MakeContextCallback("User"));
|
||||||
},
|
},
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
removeButton("CopyRawMessage");
|
removeButton("CopyRawMessage");
|
||||||
|
removeContextMenuPatch("guild-context", MakeContextCallback("Guild"));
|
||||||
|
removeContextMenuPatch("channel-context", MakeContextCallback("Channel"));
|
||||||
|
removeContextMenuPatch("user-context", MakeContextCallback("User"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -19,7 +19,6 @@
|
|||||||
import "./styles.css";
|
import "./styles.css";
|
||||||
|
|
||||||
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
|
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
|
||||||
import { Flex } from "@components/Flex";
|
|
||||||
import { Microphone } from "@components/Icons";
|
import { Microphone } from "@components/Icons";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/modal";
|
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/modal";
|
||||||
@ -39,6 +38,7 @@ import { VoiceRecorderWeb } from "./WebRecorder";
|
|||||||
const CloudUpload = findLazy(m => m.prototype?.uploadFileToCloud);
|
const CloudUpload = findLazy(m => m.prototype?.uploadFileToCloud);
|
||||||
const MessageCreator = findByPropsLazy("getSendMessageOptionsForReply", "sendMessage");
|
const MessageCreator = findByPropsLazy("getSendMessageOptionsForReply", "sendMessage");
|
||||||
const PendingReplyStore = findStoreLazy("PendingReplyStore");
|
const PendingReplyStore = findStoreLazy("PendingReplyStore");
|
||||||
|
const OptionClasses = findByPropsLazy("optionName", "optionIcon", "optionLabel");
|
||||||
|
|
||||||
export type VoiceRecorder = ComponentType<{
|
export type VoiceRecorder = ComponentType<{
|
||||||
setAudioBlob(blob: Blob): void;
|
setAudioBlob(blob: Blob): void;
|
||||||
@ -226,12 +226,10 @@ const ctxMenuPatch: NavContextMenuPatchCallback = (children, props) => () => {
|
|||||||
<Menu.MenuItem
|
<Menu.MenuItem
|
||||||
id="vc-send-vmsg"
|
id="vc-send-vmsg"
|
||||||
label={
|
label={
|
||||||
<>
|
<div className={OptionClasses.optionLabel}>
|
||||||
<Flex flexDirection="row" style={{ alignItems: "center", gap: 8 }}>
|
<Microphone className={OptionClasses.optionIcon} height={24} width={24} />
|
||||||
<Microphone height={24} width={24} />
|
<div className={OptionClasses.optionName}>Send voice message</div>
|
||||||
Send voice message
|
</div>
|
||||||
</Flex>
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
action={() => openModal(modalProps => <Modal modalProps={modalProps} />)}
|
action={() => openModal(modalProps => <Modal modalProps={modalProps} />)}
|
||||||
/>
|
/>
|
||||||
|
@ -343,6 +343,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
|||||||
name: "Rini",
|
name: "Rini",
|
||||||
id: 1079479184478441643n
|
id: 1079479184478441643n
|
||||||
},
|
},
|
||||||
|
castdrian: {
|
||||||
|
name: "castdrian",
|
||||||
|
id: 224617799434108928n
|
||||||
|
},
|
||||||
Arrow: {
|
Arrow: {
|
||||||
name: "arrow",
|
name: "arrow",
|
||||||
id: 958158495302176778n
|
id: 958158495302176778n
|
||||||
@ -351,6 +355,26 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
|||||||
name: "bb010g",
|
name: "bb010g",
|
||||||
id: 72791153467990016n,
|
id: 72791153467990016n,
|
||||||
},
|
},
|
||||||
|
Lumap: {
|
||||||
|
name: "lumap",
|
||||||
|
id: 635383782576357407n
|
||||||
|
},
|
||||||
|
Dolfies: {
|
||||||
|
name: "Dolfies",
|
||||||
|
id: 852892297661906993n,
|
||||||
|
},
|
||||||
|
RuukuLada: {
|
||||||
|
name: "RuukuLada",
|
||||||
|
id: 119705748346241027n,
|
||||||
|
},
|
||||||
|
blahajZip: {
|
||||||
|
name: "blahaj.zip",
|
||||||
|
id: 683954422241427471n,
|
||||||
|
},
|
||||||
|
archeruwu: {
|
||||||
|
name: "archer_uwu",
|
||||||
|
id: 160068695383736320n
|
||||||
|
}
|
||||||
} satisfies Record<string, Dev>);
|
} satisfies Record<string, Dev>);
|
||||||
|
|
||||||
// iife so #__PURE__ works correctly
|
// iife so #__PURE__ works correctly
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
import { MessageObject } from "@api/MessageEvents";
|
import { MessageObject } from "@api/MessageEvents";
|
||||||
import { findByCodeLazy, findByPropsLazy, findLazy } from "@webpack";
|
import { findByCodeLazy, findByPropsLazy, findLazy } from "@webpack";
|
||||||
import { ChannelStore, ComponentDispatch, GuildStore, MaskedLink, ModalImageClasses, PrivateChannelsStore, SelectedChannelStore, SelectedGuildStore, UserUtils } from "@webpack/common";
|
import { ChannelStore, ComponentDispatch, FluxDispatcher, GuildStore, MaskedLink, ModalImageClasses, PrivateChannelsStore, RestAPI, SelectedChannelStore, SelectedGuildStore, UserProfileStore, UserUtils } from "@webpack/common";
|
||||||
import { Guild, Message, User } from "discord-types/general";
|
import { Guild, Message, User } from "discord-types/general";
|
||||||
|
|
||||||
import { ImageModal, ModalRoot, ModalSize, openModal } from "./modal";
|
import { ImageModal, ModalRoot, ModalSize, openModal } from "./modal";
|
||||||
@ -118,6 +118,41 @@ export async function openUserProfile(id: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FetchUserProfileOptions {
|
||||||
|
friend_token?: string;
|
||||||
|
connections_role_id?: string;
|
||||||
|
guild_id?: string;
|
||||||
|
with_mutual_guilds?: boolean;
|
||||||
|
with_mutual_friends_count?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a user's profile
|
||||||
|
*/
|
||||||
|
export async function fetchUserProfile(id: string, options?: FetchUserProfileOptions) {
|
||||||
|
const cached = UserProfileStore.getUserProfile(id);
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
|
FluxDispatcher.dispatch({ type: "USER_PROFILE_FETCH_START", userId: id });
|
||||||
|
|
||||||
|
const { body } = await RestAPI.get({
|
||||||
|
url: `/users/${id}/profile`,
|
||||||
|
query: {
|
||||||
|
with_mutual_guilds: false,
|
||||||
|
with_mutual_friends_count: false,
|
||||||
|
...options
|
||||||
|
},
|
||||||
|
oldFormErrors: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
FluxDispatcher.dispatch({ type: "USER_UPDATE", user: body.user });
|
||||||
|
await FluxDispatcher.dispatch({ type: "USER_PROFILE_FETCH_SUCCESS", ...body });
|
||||||
|
if (options?.guild_id && body.guild_member)
|
||||||
|
FluxDispatcher.dispatch({ type: "GUILD_MEMBER_PROFILE_UPDATE", guildId: options.guild_id, guildMember: body.guild_member });
|
||||||
|
|
||||||
|
return UserProfileStore.getUserProfile(id);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the unique username for a user. Returns user.username for pomelo people, user.tag otherwise
|
* Get the unique username for a user. Returns user.username for pomelo people, user.tag otherwise
|
||||||
*/
|
*/
|
||||||
|
@ -74,6 +74,16 @@ export function isObject(obj: unknown): obj is object {
|
|||||||
return typeof obj === "object" && obj !== null && !Array.isArray(obj);
|
return typeof obj === "object" && obj !== null && !Array.isArray(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an object is empty or in other words has no own properties
|
||||||
|
*/
|
||||||
|
export function isObjectEmpty(obj: object) {
|
||||||
|
for (const k in obj)
|
||||||
|
if (Object.hasOwn(obj, k)) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns null if value is not a URL, otherwise return URL object.
|
* Returns null if value is not a URL, otherwise return URL object.
|
||||||
* Avoids having to wrap url checks in a try/catch
|
* Avoids having to wrap url checks in a try/catch
|
||||||
|
@ -63,7 +63,7 @@ export async function update() {
|
|||||||
export const getRepo = () => Unwrap(VencordNative.updater.getRepo());
|
export const getRepo = () => Unwrap(VencordNative.updater.getRepo());
|
||||||
|
|
||||||
export async function maybePromptToUpdate(confirmMessage: string, checkForDev = false) {
|
export async function maybePromptToUpdate(confirmMessage: string, checkForDev = false) {
|
||||||
if (IS_WEB) return;
|
if (IS_WEB || IS_UPDATER_DISABLED) return;
|
||||||
if (checkForDev && IS_DEV) return;
|
if (checkForDev && IS_DEV) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -48,6 +48,7 @@ export let PoggerModeSettingsStore: GenericStore;
|
|||||||
|
|
||||||
export let GuildStore: Stores.GuildStore & t.FluxStore;
|
export let GuildStore: Stores.GuildStore & t.FluxStore;
|
||||||
export let UserStore: Stores.UserStore & t.FluxStore;
|
export let UserStore: Stores.UserStore & t.FluxStore;
|
||||||
|
export let UserProfileStore: GenericStore;
|
||||||
export let SelectedChannelStore: Stores.SelectedChannelStore & t.FluxStore;
|
export let SelectedChannelStore: Stores.SelectedChannelStore & t.FluxStore;
|
||||||
export let SelectedGuildStore: t.FluxStore & Record<string, any>;
|
export let SelectedGuildStore: t.FluxStore & Record<string, any>;
|
||||||
export let ChannelStore: Stores.ChannelStore & t.FluxStore;
|
export let ChannelStore: Stores.ChannelStore & t.FluxStore;
|
||||||
@ -86,6 +87,7 @@ export const useStateFromStores: <T>(
|
|||||||
|
|
||||||
waitForStore("DraftStore", s => DraftStore = s);
|
waitForStore("DraftStore", s => DraftStore = s);
|
||||||
waitForStore("UserStore", s => UserStore = s);
|
waitForStore("UserStore", s => UserStore = s);
|
||||||
|
waitForStore("UserProfileStore", m => UserProfileStore = m);
|
||||||
waitForStore("ChannelStore", m => ChannelStore = m);
|
waitForStore("ChannelStore", m => ChannelStore = m);
|
||||||
waitForStore("SelectedChannelStore", m => SelectedChannelStore = m);
|
waitForStore("SelectedChannelStore", m => SelectedChannelStore = m);
|
||||||
waitForStore("SelectedGuildStore", m => SelectedGuildStore = m);
|
waitForStore("SelectedGuildStore", m => SelectedGuildStore = m);
|
||||||
|
16
src/webpack/common/types/components.d.ts
vendored
16
src/webpack/common/types/components.d.ts
vendored
@ -398,12 +398,18 @@ export type Paginator = ComponentType<{
|
|||||||
hideMaxPage?: boolean;
|
hideMaxPage?: boolean;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type MaskedLink = ComponentType<{
|
export type MaskedLink = ComponentType<PropsWithChildren<{
|
||||||
onClick(): void;
|
|
||||||
trusted: boolean;
|
|
||||||
title: string,
|
|
||||||
href: string;
|
href: string;
|
||||||
}>;
|
rel?: string;
|
||||||
|
target?: string;
|
||||||
|
title?: string,
|
||||||
|
className?: string;
|
||||||
|
tabIndex?: number;
|
||||||
|
onClick?(): void;
|
||||||
|
trusted?: boolean;
|
||||||
|
messageId?: string;
|
||||||
|
channelId?: string;
|
||||||
|
}>>;
|
||||||
|
|
||||||
export type ScrollerThin = ComponentType<PropsWithChildren<{
|
export type ScrollerThin = ComponentType<PropsWithChildren<{
|
||||||
className?: string;
|
className?: string;
|
||||||
|
9
src/webpack/common/types/menu.d.ts
vendored
9
src/webpack/common/types/menu.d.ts
vendored
@ -65,8 +65,13 @@ export interface Menu {
|
|||||||
id: string;
|
id: string;
|
||||||
interactive?: boolean;
|
interactive?: boolean;
|
||||||
}>;
|
}>;
|
||||||
// TODO: Type me
|
MenuSliderControl: RC<{
|
||||||
MenuSliderControl: RC<any>;
|
minValue: number,
|
||||||
|
maxValue: number,
|
||||||
|
value: number,
|
||||||
|
onChange(value: number): void,
|
||||||
|
renderValue?(value: number): string,
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContextMenuApi {
|
export interface ContextMenuApi {
|
||||||
|
Reference in New Issue
Block a user