Compare commits

..

16 Commits

Author SHA1 Message Date
V
16365d3ea1 bump to 1.2.2 2023-05-13 18:48:40 +02:00
V
1ec28a345b EmoteCloner: Add Sticker cloning (#1118) 2023-05-13 18:47:46 +02:00
V
2fdc00b11e BadgesAPI: Fix canary crash (new pomelo badge, the horror) 2023-05-12 23:34:30 +02:00
V
3da112680d oops, add missing else 2023-05-12 04:17:12 +02:00
V
1d93162036 MessageClickActions: Add double click to reply 2023-05-12 04:15:35 +02:00
V
7dcd32e838 PlatformIndicators: Fix weird spacing in badges 2023-05-12 03:54:57 +02:00
V
ade31f993b Implement plugin tags 2023-05-12 03:41:15 +02:00
AutumnVN
3c7496ac6d TextReplace: Visible Linebreak in settings (#1063)
Co-authored-by: V <vendicated@riseup.net>
2023-05-12 02:59:51 +02:00
Supertiger
63387a48ee silentMessageToggle: Add setting to disable auto disable (#1062)
Co-authored-by: V <vendicated@riseup.net>
2023-05-12 00:58:53 +00:00
V
3bb68467bb Update README.md 2023-05-12 02:40:31 +02:00
V
2b337eace1 [skip ci] ShowHiddenChannel: Fix broken patch 2023-05-12 02:30:51 +02:00
V
5c5b009c41 Settings: Fix resetting scroll/search when getting a ping (#1106) 2023-05-12 01:40:43 +02:00
Nuckyz
0c54b1fa1d [skip ci] Fix InvisibleChat button being added to wrong chat box (#1100) 2023-05-11 17:44:33 +00:00
Kode
393f76749a USRBG: Hide Nitro badge if banner's source is USRBG (#1096)
* Hide Nitro badge if banner's source is USRBG, tweaks to description

* Rename function

* Update src/plugins/usrbg/index.tsx

---------

Co-authored-by: V <vendicated@riseup.net>
2023-05-11 19:33:08 +02:00
V
1fe7f3c297 ViewIcons: More consistent context menu position 2023-05-11 19:26:55 +02:00
V
622e8dc3e0 [skip ci] Translate: Shift/Right click shortcut 2023-05-11 19:00:18 +02:00
35 changed files with 437 additions and 309 deletions

View File

@ -26,5 +26,8 @@ jobs:
- name: Lint & Test if desktop version compiles
run: pnpm test
- name: Lint & Test if web version compiles
run: pnpm testWeb
- name: Test if web version compiles
run: pnpm buildWeb
- name: Test if plugin structure is valid
run: pnpm generatePluginJson

View File

@ -10,7 +10,7 @@ The cutest Discord client mod
- Super easy to install (Download Installer, open, click install button, done)
- 100+ plugins built in: [See a list](https://vencord.dev/plugins)
- Some highlights: SpotifyControls, GameActivityToggle, Experiments, NoTrack, MessageLogger, QuickReply, Free Emotes/Stickers, CustomCommands, ShowHiddenChannels, PronounDB
- Some highlights: SpotifyControls, MessageLogger, Experiments, GameActivityToggle, Translate, NoTrack, QuickReply, Free Emotes/Stickers, CustomCommands, ShowHiddenChannels, PronounDB
- Fairly lightweight despite the many inbuilt plugins
- Excellent Browser Support: Run Vencord in your Browser via extension or UserScript
- Works on any Discord branch: Stable, Canary or PTB all work (though for the best experience I recommend stable!)

View File

@ -1,7 +1,7 @@
{
"name": "vencord",
"private": "true",
"version": "1.2.1",
"version": "1.2.2",
"description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": {

View File

@ -29,6 +29,7 @@ interface Dev {
interface PluginData {
name: string;
description: string;
tags: string[];
authors: Dev[];
dependencies: string[];
hasPatches: boolean;
@ -106,6 +107,7 @@ async function parseFile(fileName: string) {
hasCommands: false,
enabledByDefault: false,
required: false,
tags: [] as string[]
} as PluginData;
for (const prop of pluginObj.properties) {
@ -131,6 +133,13 @@ async function parseFile(fileName: string) {
return devs[getName(e)!];
});
break;
case "tags":
if (!isArrayLiteralExpression(value)) throw fail("tags is not an array literal");
data.tags = value.elements.map(e => {
if (!isStringLiteral(e)) throw fail("tags array contains non-string literals");
return e.text;
});
break;
case "dependencies":
if (!isArrayLiteralExpression(value)) throw fail("dependencies is not an array literal");
const { elements } = value;

View File

@ -37,8 +37,6 @@ import { checkForUpdates, update, UpdateLogger } from "./utils/updater";
import { onceReady } from "./webpack";
import { SettingsRouter } from "./webpack/common";
export let Components: any;
async function syncSettings() {
if (
Settings.cloud.settingsSync && // if it's enabled
@ -65,7 +63,6 @@ async function syncSettings() {
async function init() {
await onceReady;
startAllPlugins();
Components = await import("./components");
syncSettings();

View File

@ -28,6 +28,7 @@ import * as $MessagePopover from "./MessagePopover";
import * as $Notices from "./Notices";
import * as $Notifications from "./Notifications";
import * as $ServerList from "./ServerList";
import * as $Settings from "./Settings";
import * as $SettingsStore from "./SettingsStore";
import * as $Styles from "./Styles";
@ -86,6 +87,10 @@ export const MessageDecorations = $MessageDecorations;
* An API allowing you to add components to member list users, in both DM's and servers
*/
export const MemberListDecorators = $MemberListDecorators;
/**
* An API allowing you to persist data
*/
export const Settings = $Settings;
/**
* An API allowing you to read, manipulate and automatically update components based on Discord settings
*/

View File

@ -20,20 +20,18 @@ import "./styles.css";
import * as DataStore from "@api/DataStore";
import { showNotice } from "@api/Notices";
import { useSettings } from "@api/Settings";
import { Settings, useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { handleComponentFailed } from "@components/handleComponentFailed";
import { Badge } from "@components/PluginSettings/components";
import PluginModal from "@components/PluginSettings/PluginModal";
import { Switch } from "@components/Switch";
import { SettingsTab } from "@components/VencordSettings/shared";
import { ChangeList } from "@utils/ChangeList";
import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { openModalLazy } from "@utils/modal";
import { onlyOnce } from "@utils/onlyOnce";
import { LazyComponent, useAwaiter } from "@utils/react";
import { Plugin } from "@utils/types";
import { findByCode, findByPropsLazy } from "@webpack";
@ -96,7 +94,7 @@ interface PluginCardProps extends React.HTMLProps<HTMLDivElement> {
}
function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave, isNew }: PluginCardProps) {
const settings = useSettings([`plugins.${plugin.name}.enabled`]).plugins[plugin.name];
const settings = Settings.plugins[plugin.name];
const isEnabled = () => settings.enabled ?? false;
@ -179,7 +177,7 @@ enum SearchStatus {
DISABLED
}
export default ErrorBoundary.wrap(function PluginSettings() {
export default function PluginSettings() {
const settings = useSettings();
const changes = React.useMemo(() => new ChangeList<string>(), []);
@ -230,9 +228,12 @@ export default ErrorBoundary.wrap(function PluginSettings() {
if (enabled && searchValue.status === SearchStatus.DISABLED) return false;
if (!enabled && searchValue.status === SearchStatus.ENABLED) return false;
if (!searchValue.value.length) return true;
const v = searchValue.value.toLowerCase();
return (
plugin.name.toLowerCase().includes(searchValue.value.toLowerCase()) ||
plugin.description.toLowerCase().includes(searchValue.value.toLowerCase())
plugin.name.toLowerCase().includes(v) ||
plugin.description.toLowerCase().includes(v) ||
plugin.tags?.some(t => t.toLowerCase().includes(v))
);
};
@ -303,7 +304,7 @@ export default ErrorBoundary.wrap(function PluginSettings() {
}
return (
<Forms.FormSection className={Margins.top16}>
<SettingsTab title="Plugins">
<ReloadRequiredCard required={changes.hasChanges} />
<Forms.FormTitle tag="h5" className={classes(Margins.top20, Margins.bottom8)}>
@ -342,12 +343,9 @@ export default ErrorBoundary.wrap(function PluginSettings() {
<div className={cl("grid")}>
{requiredPlugins}
</div>
</Forms.FormSection >
</SettingsTab >
);
}, {
message: "Failed to render the Plugin Settings. If this persists, try using the installer to reinstall!",
onError: onlyOnce(handleComponentFailed),
});
}
function makeDependencyList(deps: string[]) {
return (

View File

@ -16,16 +16,17 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { downloadSettingsBackup, uploadSettingsBackup } from "@utils/settingsSync";
import { Button, Card, Forms, Text } from "@webpack/common";
import { Button, Card, Text } from "@webpack/common";
import { SettingsTab, wrapTab } from "./shared";
function BackupRestoreTab() {
return (
<Forms.FormSection title="Settings Sync" className={Margins.top16}>
<SettingsTab title="Backup & Restore">
<Card className={classes("vc-settings-card", "vc-backup-restore-card")}>
<Flex flexDirection="column">
<strong>Warning</strong>
@ -59,8 +60,8 @@ function BackupRestoreTab() {
Export Settings
</Button>
</Flex>
</Forms.FormSection>
</SettingsTab>
);
}
export default ErrorBoundary.wrap(BackupRestoreTab);
export default wrapTab(BackupRestoreTab, "Backup & Restore");

View File

@ -19,13 +19,14 @@
import { showNotification } from "@api/Notifications";
import { Settings, useSettings } from "@api/Settings";
import { CheckedTextInput } from "@components/CheckedTextInput";
import ErrorBoundary from "@components/ErrorBoundary";
import { Link } from "@components/Link";
import { authorizeCloud, cloudLogger, deauthorizeCloud, getCloudAuth, getCloudUrl } from "@utils/cloud";
import { Margins } from "@utils/margins";
import { deleteCloudSettings, getCloudSettings, putCloudSettings } from "@utils/settingsSync";
import { Alerts, Button, Forms, Switch, Tooltip } from "@webpack/common";
import { SettingsTab, wrapTab } from "./shared";
function validateUrl(url: string) {
try {
new URL(url);
@ -114,7 +115,7 @@ function CloudTab() {
const settings = useSettings(["cloud.authenticated", "cloud.url"]);
return (
<>
<SettingsTab title="Vencord Cloud">
<Forms.FormSection title="Cloud Settings" className={Margins.top16}>
<Forms.FormText variant="text-md/normal" className={Margins.bottom20}>
Vencord comes with a cloud integration that adds goodies like settings sync across devices.
@ -157,8 +158,8 @@ function CloudTab() {
<Forms.FormDivider className={Margins.top16} />
</Forms.FormSection >
<SettingsSyncSection />
</>
</SettingsTab>
);
}
export default ErrorBoundary.wrap(CloudTab);
export default wrapTab(CloudTab, "Cloud");

View File

@ -16,16 +16,16 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { CheckedTextInput } from "@components/CheckedTextInput";
import { debounce } from "@utils/debounce";
import { Margins } from "@utils/margins";
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
import { makeCodeblock } from "@utils/text";
import { ReplaceFn } from "@utils/types";
import { search } from "@webpack";
import { Button, Clipboard, Forms, Parser, React, Switch, Text, TextInput } from "@webpack/common";
import { Button, Clipboard, Forms, Parser, React, Switch, TextInput } from "@webpack/common";
import { CheckedTextInput } from "./CheckedTextInput";
import ErrorBoundary from "./ErrorBoundary";
import { SettingsTab, wrapTab } from "./shared";
// Do not include diff in non dev builds (side effects import)
if (IS_DEV) {
@ -258,8 +258,7 @@ function PatchHelper() {
}
return (
<Forms.FormSection>
<Text variant="heading-md/normal" tag="h2" className={Margins.bottom8}>Patch Helper</Text>
<SettingsTab title="Patch Helper">
<Forms.FormTitle>find</Forms.FormTitle>
<TextInput
type="text"
@ -304,8 +303,8 @@ function PatchHelper() {
<Button onClick={() => Clipboard.copy(code)}>Copy to Clipboard</Button>
</>
)}
</Forms.FormSection>
</SettingsTab>
);
}
export default IS_DEV ? ErrorBoundary.wrap(PatchHelper) : null;
export default IS_DEV ? wrapTab(PatchHelper, "PatchHelper") : null;

View File

@ -16,7 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import ErrorBoundary from "@components/ErrorBoundary";
import PluginSettings from "@components/PluginSettings";
export default ErrorBoundary.wrap(PluginSettings);
import { wrapTab } from "./shared";
export default wrapTab(PluginSettings, "Plugins");

View File

@ -17,13 +17,14 @@
*/
import { useSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Link } from "@components/Link";
import { Margins } from "@utils/margins";
import { useAwaiter } from "@utils/react";
import { findLazy } from "@webpack";
import { Card, Forms, React, TextArea } from "@webpack/common";
import { SettingsTab, wrapTab } from "./shared";
const TextAreaProps = findLazy(m => typeof m.textarea === "string");
function Validator({ link }: { link: string; }) {
@ -74,8 +75,8 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) {
);
}
export default ErrorBoundary.wrap(function () {
const settings = useSettings();
function ThemesTab() {
const settings = useSettings(["themeLinks"]);
const [themeText, setThemeText] = React.useState(settings.themeLinks.join("\n"));
function onBlur() {
@ -89,7 +90,7 @@ export default ErrorBoundary.wrap(function () {
}
return (
<>
<SettingsTab title="Themes">
<Card className="vc-settings-card vc-text-selectable">
<Forms.FormTitle tag="h5">Paste links to .theme.css files here</Forms.FormTitle>
<Forms.FormText>One link per line</Forms.FormText>
@ -124,6 +125,8 @@ export default ErrorBoundary.wrap(function () {
onBlur={onBlur}
/>
<Validators themeLinks={settings.themeLinks} />
</>
</SettingsTab>
);
});
}
export default wrapTab(ThemesTab, "Themes");

View File

@ -17,21 +17,20 @@
*/
import { useSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { ErrorCard } from "@components/ErrorCard";
import { Flex } from "@components/Flex";
import { handleComponentFailed } from "@components/handleComponentFailed";
import { Link } from "@components/Link";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { relaunch } from "@utils/native";
import { onlyOnce } from "@utils/onlyOnce";
import { useAwaiter } from "@utils/react";
import { changes, checkForUpdates, getRepo, isNewer, update, updateError, UpdateLogger } from "@utils/updater";
import { Alerts, Button, Card, Forms, Parser, React, Switch, Toasts } from "@webpack/common";
import gitHash from "~git-hash";
import { SettingsTab, wrapTab } from "./shared";
function withDispatcher(dispatcher: React.Dispatch<React.SetStateAction<boolean>>, action: () => any) {
return async () => {
dispatcher(true);
@ -199,7 +198,7 @@ function Updater() {
};
return (
<Forms.FormSection className={Margins.top16}>
<SettingsTab title="Vencord Updater">
<Forms.FormTitle tag="h5">Updater Settings</Forms.FormTitle>
<Switch
value={settings.notifyAboutUpdates}
@ -246,11 +245,8 @@ function Updater() {
<Forms.FormTitle tag="h5">Updates</Forms.FormTitle>
{isNewer ? <Newer {...commonProps} /> : <Updatable {...commonProps} />}
</Forms.FormSection >
</SettingsTab>
);
}
export default IS_WEB ? null : ErrorBoundary.wrap(Updater, {
message: "Failed to render the Updater. If this persists, try using the installer to reinstall!",
onError: onlyOnce(handleComponentFailed),
});
export default IS_WEB ? null : wrapTab(Updater, "Updater");

View File

@ -21,7 +21,6 @@ import { openNotificationLogModal } from "@api/Notifications/notificationLog";
import { Settings, useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import DonateButton from "@components/DonateButton";
import ErrorBoundary from "@components/ErrorBoundary";
import { ErrorCard } from "@components/ErrorCard";
import { Margins } from "@utils/margins";
import { identity } from "@utils/misc";
@ -29,6 +28,8 @@ import { relaunch, showItemInFolder } from "@utils/native";
import { useAwaiter } from "@utils/react";
import { Button, Card, Forms, React, Select, Slider, Switch } from "@webpack/common";
import { SettingsTab, wrapTab } from "./shared";
const cl = classNameFactory("vc-settings-");
const DEFAULT_DONATE_IMAGE = "https://cdn.discordapp.com/emojis/1026533090627174460.png";
@ -97,7 +98,7 @@ function VencordSettings() {
];
return (
<React.Fragment>
<SettingsTab title="Vencord Settings">
<DonateCard image={donateImage} />
<Forms.FormSection title="Quick Actions">
<Card className={cl("quick-actions-card")}>
@ -153,7 +154,7 @@ function VencordSettings() {
{typeof Notification !== "undefined" && <NotificationSection settings={settings.notifications} />}
</React.Fragment>
</SettingsTab>
);
}
@ -263,4 +264,4 @@ function DonateCard({ image }: DonateCardProps) {
);
}
export default ErrorBoundary.wrap(VencordSettings);
export default wrapTab(VencordSettings, "Vencord Settings");

View File

@ -1,96 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "./settingsStyles.css";
import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { handleComponentFailed } from "@components/handleComponentFailed";
import { isMobile } from "@utils/misc";
import { onlyOnce } from "@utils/onlyOnce";
import { Forms, SettingsRouter, TabBar, Text } from "@webpack/common";
import BackupRestoreTab from "./BackupRestoreTab";
import CloudTab from "./CloudTab";
import PluginsTab from "./PluginsTab";
import ThemesTab from "./ThemesTab";
import Updater from "./Updater";
import VencordSettings from "./VencordTab";
const cl = classNameFactory("vc-settings-");
interface SettingsProps {
tab: string;
}
interface SettingsTab {
name: string;
component?: React.ComponentType;
}
const SettingsTabs: Record<string, SettingsTab> = {
VencordSettings: { name: "Vencord", component: () => <VencordSettings /> },
VencordPlugins: { name: "Plugins", component: () => <PluginsTab /> },
VencordThemes: { name: "Themes", component: () => <ThemesTab /> },
VencordUpdater: { name: "Updater" }, // Only show updater if IS_WEB is false
VencordCloud: { name: "Cloud", component: () => <CloudTab /> },
VencordSettingsSync: { name: "Backup & Restore", component: () => <BackupRestoreTab /> }
};
if (!IS_WEB) SettingsTabs.VencordUpdater.component = () => Updater && <Updater />;
function Settings(props: SettingsProps) {
const { tab = "VencordSettings" } = props;
const CurrentTab = SettingsTabs[tab]?.component ?? null;
if (isMobile) {
return CurrentTab && <CurrentTab />;
}
return <Forms.FormSection>
<Text variant="heading-lg/semibold" style={{ color: "var(--header-primary)" }} tag="h2">Vencord Settings</Text>
<TabBar
type="top"
look="brand"
className={cl("tab-bar")}
selectedItem={tab}
onItemSelect={SettingsRouter.open}
>
{Object.entries(SettingsTabs).map(([key, { name, component }]) => {
if (!component) return null;
return <TabBar.Item
id={key}
className={cl("tab-bar-item")}
key={key}>
{name}
</TabBar.Item>;
})}
</TabBar>
<Forms.FormDivider />
{CurrentTab && <CurrentTab />}
</Forms.FormSection >;
}
const onError = onlyOnce(handleComponentFailed);
export default function (props: SettingsProps) {
return <ErrorBoundary onError={onError}>
<Settings tab={props.tab} />
</ErrorBoundary>;
}

View File

@ -29,14 +29,12 @@
.vc-settings-card {
padding: 1em;
margin-bottom: 1em;
margin-top: 1em;
}
.vc-backup-restore-card {
background-color: var(--info-warning-background);
border-color: var(--info-warning-foreground);
color: var(--info-warning-text);
margin-top: 0;
}
.vc-settings-theme-links {

View File

@ -0,0 +1,51 @@
/*
* 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 "./settingsStyles.css";
import ErrorBoundary from "@components/ErrorBoundary";
import { handleComponentFailed } from "@components/handleComponentFailed";
import { Margins } from "@utils/margins";
import { onlyOnce } from "@utils/onlyOnce";
import { Forms, Text } from "@webpack/common";
import type { ComponentType, PropsWithChildren } from "react";
export function SettingsTab({ title, children }: PropsWithChildren<{ title: string; }>) {
return (
<Forms.FormSection>
<Text
variant="heading-lg/semibold"
tag="h2"
className={Margins.bottom16}
>
{title}
</Text>
{children}
</Forms.FormSection>
);
}
const onError = onlyOnce(handleComponentFailed);
export function wrapTab(component: ComponentType, tab: string) {
return ErrorBoundary.wrap(component, {
message: `Failed to render the ${tab} tab. If this issue persists, try using the installer to reinstall!`,
onError,
});
}

View File

@ -1,21 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export { default as PatchHelper } from "./PatchHelper";
export { default as PluginSettings } from "./PluginSettings";
export { default as VencordSettings } from "./VencordSettings";

View File

@ -82,8 +82,8 @@ export default definePlugin({
find: "Messages.PROFILE_USER_BADGES,role:",
replacement: [
{
match: /null==\i\?void 0:(\i)\.getBadges\(\)/,
replace: (_, badgesMod) => `Vencord.Api.Badges._getBadges(arguments[0]).concat(${badgesMod}?.getBadges()??[])`,
match: /(?<=(\i)\.isTryItOutFlow,)(.{0,300})null==\i\?void 0:(\i)\.getBadges\(\)/,
replace: (_, props, restCode, badgesMod) => `vencordProps=${props},${restCode}Vencord.Api.Badges._getBadges(vencordProps).concat(${badgesMod}?.getBadges()??[])`,
},
{
// alt: "", aria-hidden: false, src: originalSrc

View File

@ -21,17 +21,102 @@ import { CheckedTextInput } from "@components/CheckedTextInput";
import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins";
import { ModalContent, ModalHeader, ModalRoot, openModal } from "@utils/modal";
import { ModalContent, ModalHeader, ModalRoot, openModalLazy } from "@utils/modal";
import definePlugin from "@utils/types";
import { findByCodeLazy, findByPropsLazy } from "@webpack";
import { Forms, GuildStore, Menu, PermissionStore, React, Toasts, Tooltip, UserStore } from "@webpack/common";
import { findByCodeLazy, findStoreLazy } from "@webpack";
import { FluxDispatcher, Forms, GuildStore, Menu, PermissionStore, React, RestAPI, Toasts, Tooltip, UserStore } from "@webpack/common";
import { Promisable } from "type-fest";
const MANAGE_EMOJIS_AND_STICKERS = 1n << 30n;
const GuildEmojiStore = findByPropsLazy("getGuilds", "getGuildEmoji");
const GuildEmojiStore = findStoreLazy("EmojiStore");
const StickersStore = findStoreLazy("StickersStore");
const uploadEmoji = findByCodeLazy('"EMOJI_UPLOAD_START"', "GUILD_EMOJIS(");
function getGuildCandidates(isAnimated: boolean) {
interface Sticker {
t: "Sticker";
description: string;
format_type: number;
guild_id: string;
id: string;
name: string;
tags: string;
type: number;
}
interface Emoji {
t: "Emoji";
id: string;
name: string;
isAnimated: boolean;
}
type Data = Emoji | Sticker;
const StickerExt = [, "png", "png", "json", "gif"] as const;
function getUrl(data: Data) {
if (data.t === "Emoji")
return `${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/emojis/${data.id}.${data.isAnimated ? "gif" : "png"}`;
return `${location.origin}/stickers/${data.id}.${StickerExt[data.format_type]}`;
}
async function fetchSticker(id: string) {
const cached = StickersStore.getStickerById(id);
if (cached) return cached;
const { body } = await RestAPI.get({
url: `/stickers/${id}`
});
FluxDispatcher.dispatch({
type: "STICKER_FETCH_SUCCESS",
sticker: body
});
return body as Sticker;
}
async function cloneSticker(guildId: string, sticker: Sticker) {
const data = new FormData();
data.append("name", sticker.name);
data.append("tags", sticker.tags);
data.append("description", sticker.description);
data.append("file", await fetchBlob(getUrl(sticker)));
const { body } = await RestAPI.post({
url: `/guilds/${guildId}/stickers`,
body: data,
});
FluxDispatcher.dispatch({
type: "GUILD_STICKERS_CREATE_SUCCESS",
guildId,
sticker: {
...body,
user: UserStore.getCurrentUser()
}
});
}
async function cloneEmoji(guildId: string, emoji: Emoji) {
const data = await fetchBlob(getUrl(emoji));
const dataUrl = await new Promise<string>(resolve => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.readAsDataURL(data);
});
return uploadEmoji({
guildId,
name: emoji.name.split("~")[0],
image: dataUrl
});
}
function getGuildCandidates(data: Data) {
const meId = UserStore.getCurrentUser().id;
return Object.values(GuildStore.getGuilds()).filter(g => {
@ -39,6 +124,10 @@ function getGuildCandidates(isAnimated: boolean) {
BigInt(PermissionStore.getGuildPermissions({ id: g.id }) & MANAGE_EMOJIS_AND_STICKERS) === MANAGE_EMOJIS_AND_STICKERS;
if (!canCreate) return false;
if (data.t === "Sticker") return true;
const { isAnimated } = data as Emoji;
const emojiSlots = g.getMaxEmojiSlots();
const { emojis } = GuildEmojiStore.getGuilds()[g.id];
@ -49,33 +138,34 @@ function getGuildCandidates(isAnimated: boolean) {
}).sort((a, b) => a.name.localeCompare(b.name));
}
async function doClone(guildId: string, id: string, name: string, isAnimated: boolean) {
const data = await fetch(`${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/emojis/${id}.${isAnimated ? "gif" : "png"}`)
.then(r => r.blob());
const reader = new FileReader();
async function fetchBlob(url: string) {
const res = await fetch(url);
if (!res.ok)
throw new Error(`Failed to fetch ${url} - ${res.status}`);
reader.onload = () => {
uploadEmoji({
guildId,
name: name.split("~")[0],
image: reader.result
}).then(() => {
Toasts.show({
message: `Successfully cloned ${name}!`,
type: Toasts.Type.SUCCESS,
id: Toasts.genId()
});
}).catch((e: any) => {
new Logger("EmoteCloner").error("Failed to upload emoji", e);
Toasts.show({
message: "Oopsie something went wrong :( Check console!!!",
type: Toasts.Type.FAILURE,
id: Toasts.genId()
});
return res.blob();
}
async function doClone(guildId: string, data: Sticker | Emoji) {
try {
if (data.t === "Sticker")
await cloneSticker(guildId, data);
else
await cloneEmoji(guildId, data);
Toasts.show({
message: `Successfully cloned ${data.name} to ${GuildStore.getGuild(guildId)?.name ?? "your server"}!`,
type: Toasts.Type.SUCCESS,
id: Toasts.genId()
});
};
reader.readAsDataURL(data);
} catch (e) {
new Logger("EmoteCloner").error("Failed to clone", data.name, "to", guildId, e);
Toasts.show({
message: "Oopsie something went wrong :( Check console!!!",
type: Toasts.Type.FAILURE,
id: Toasts.genId()
});
}
}
const getFontSize = (s: string) => {
@ -86,20 +176,23 @@ const getFontSize = (s: string) => {
const nameValidator = /^\w+$/i;
function CloneModal({ id, name: emojiName, isAnimated }: { id: string; name: string; isAnimated: boolean; }) {
function CloneModal({ data }: { data: Sticker | Emoji; }) {
const [isCloning, setIsCloning] = React.useState(false);
const [name, setName] = React.useState(emojiName);
const [name, setName] = React.useState(data.name);
const [x, invalidateMemo] = React.useReducer(x => x + 1, 0);
const guilds = React.useMemo(() => getGuildCandidates(isAnimated), [isAnimated, x]);
const guilds = React.useMemo(() => getGuildCandidates(data), [data.id, x]);
return (
<>
<Forms.FormTitle className={Margins.top20}>Custom Name</Forms.FormTitle>
<CheckedTextInput
value={name}
onChange={setName}
onChange={v => {
data.name = v;
setName(v);
}}
validate={v =>
(v.length > 1 && v.length < 32 && nameValidator.test(v))
|| "Name must be between 2 and 32 characters and only contain alphanumeric characters"
@ -135,7 +228,7 @@ function CloneModal({ id, name: emojiName, isAnimated }: { id: string; name: str
}}
onClick={isCloning ? void 0 : async () => {
setIsCloning(true);
doClone(g.id, id, name, isAnimated).finally(() => {
doClone(g.id, data).finally(() => {
invalidateMemo();
setIsCloning(false);
});
@ -175,32 +268,38 @@ function CloneModal({ id, name: emojiName, isAnimated }: { id: string; name: str
);
}
function buildMenuItem(id: string, name: string, isAnimated: boolean) {
function buildMenuItem(type: "Emoji" | "Sticker", fetchData: () => Promisable<Omit<Sticker | Emoji, "t">>) {
return (
<Menu.MenuItem
id="emote-cloner"
key="emote-cloner"
label="Clone Emote"
label={`Clone ${type}`}
action={() =>
openModal(modalProps => (
<ModalRoot {...modalProps}>
<ModalHeader>
<img
role="presentation"
aria-hidden
src={`${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/emojis/${id}.${isAnimated ? "gif" : "png"}`}
alt=""
height={24}
width={24}
style={{ marginRight: "0.5em" }}
/>
<Forms.FormText>Clone {name}</Forms.FormText>
</ModalHeader>
<ModalContent>
<CloneModal id={id} name={name} isAnimated={isAnimated} />
</ModalContent>
</ModalRoot>
))
openModalLazy(async () => {
const res = await fetchData();
const data = { t: type, ...res } as Sticker | Emoji;
const url = getUrl(data);
return modalProps => (
<ModalRoot {...modalProps}>
<ModalHeader>
<img
role="presentation"
aria-hidden
src={url}
alt=""
height={24}
width={24}
style={{ marginRight: "0.5em" }}
/>
<Forms.FormText>Clone {data.name}</Forms.FormText>
</ModalHeader>
<ModalContent>
<CloneModal data={data} />
</ModalContent>
</ModalRoot>
);
})
}
/>
);
@ -213,28 +312,53 @@ function isGifUrl(url: string) {
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => {
const { favoriteableId, itemHref, itemSrc, favoriteableType } = props ?? {};
if (!favoriteableId || favoriteableType !== "emoji") return;
if (!favoriteableId) return;
const match = props.message.content.match(RegExp(`<a?:(\\w+)(?:~\\d+)?:${favoriteableId}>|https://cdn\\.discordapp\\.com/emojis/${favoriteableId}\\.`));
if (!match) return;
const name = match[1] ?? "FakeNitroEmoji";
const menuItem = (() => {
switch (favoriteableType) {
case "emoji":
const match = props.message.content.match(RegExp(`<a?:(\\w+)(?:~\\d+)?:${favoriteableId}>|https://cdn\\.discordapp\\.com/emojis/${favoriteableId}\\.`));
if (!match) return;
const name = match[1] ?? "FakeNitroEmoji";
const group = findGroupChildrenByChildId("copy-link", children);
if (group) group.push(buildMenuItem(favoriteableId, name, isGifUrl(itemHref ?? itemSrc)));
return buildMenuItem("Emoji", () => ({
id: favoriteableId,
name,
isAnimated: isGifUrl(itemHref ?? itemSrc)
}));
case "sticker":
const sticker = props.message.stickerItems.find(s => s.id === favoriteableId);
if (sticker?.format_type === 3 /* LOTTIE */) return;
return buildMenuItem("Sticker", () => fetchSticker(favoriteableId));
}
})();
if (menuItem)
findGroupChildrenByChildId("copy-link", children)?.push(menuItem);
};
const expressionPickerPatch: NavContextMenuPatchCallback = (children, props: { target: HTMLElement; }) => () => {
const { id, name, type } = props?.target?.dataset ?? {};
if (!id || !name || type !== "emoji") return;
if (!id) return;
const firstChild = props.target.firstChild as HTMLImageElement;
if (type === "emoji" && name) {
const firstChild = props.target.firstChild as HTMLImageElement;
children.push(buildMenuItem(id, name, firstChild && isGifUrl(firstChild.src)));
children.push(buildMenuItem("Emoji", () => ({
id,
name,
isAnimated: firstChild && isGifUrl(firstChild.src)
})));
} else if (type === "sticker" && !props.target.className?.includes("lottieCanvas")) {
children.push(buildMenuItem("Sticker", () => fetchSticker(id)));
}
};
export default definePlugin({
name: "EmoteCloner",
description: "Adds a Clone context menu item to emotes to clone them your own server",
description: "Allows you to clone Emotes & Stickers to your own server (right click them)",
tags: ["StickerCloner"],
authors: [Devs.Ven, Devs.Nuckyz],
start() {

View File

@ -130,6 +130,8 @@ export default definePlugin({
name: "ImageZoom",
description: "Lets you zoom in to images and gifs. Use scroll wheel to zoom in and shift + scroll wheel to increase lens radius / size",
authors: [Devs.Aria],
tags: ["ImageUtilities"],
patches: [
{
find: '"renderLinkComponent","maxWidth"',

View File

@ -64,7 +64,13 @@ function Indicator() {
}
function ChatBarIcon() {
function ChatBarIcon(chatBoxProps: {
type: {
analyticsName: string;
};
}) {
if (chatBoxProps.type.analyticsName !== "normal") return null;
return (
<Tooltip text="Encrypt Message">
{({ onMouseEnter, onMouseLeave }) => (
@ -133,7 +139,7 @@ export default definePlugin({
find: ".activeCommandOption",
replacement: {
match: /(.)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/,
replace: "$&;try{$2||$1.push($self.chatBarIcon())}catch{}",
replace: "$&;try{$2||$1.push($self.chatBarIcon(arguments[0]))}catch{}",
}
},
],

View File

@ -17,10 +17,11 @@
*/
import { addClickListener, removeClickListener } from "@api/MessageEvents";
import { definePluginSettings, Settings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { PermissionStore, UserStore } from "@webpack/common";
import { FluxDispatcher, PermissionStore, UserStore } from "@webpack/common";
let isDeletePressed = false;
const keydown = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = true);
@ -28,24 +29,36 @@ const keyup = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed =
const MANAGE_CHANNELS = 1n << 4n;
const settings = definePluginSettings({
enableDeleteOnClick: {
type: OptionType.BOOLEAN,
description: "Enable delete on click",
default: true
},
enableDoubleClickToEdit: {
type: OptionType.BOOLEAN,
description: "Enable double click to edit",
default: true
},
enableDoubleClickToReply: {
type: OptionType.BOOLEAN,
description: "Enable double click to reply",
default: true
},
requireModifier: {
type: OptionType.BOOLEAN,
description: "Only do double click actions when shift/ctrl is held",
default: false
}
});
export default definePlugin({
name: "MessageClickActions",
description: "Hold Backspace and click to delete, double click to edit",
description: "Hold Backspace and click to delete, double click to edit/reply",
authors: [Devs.Ven],
dependencies: ["MessageEventsAPI"],
options: {
enableDeleteOnClick: {
type: OptionType.BOOLEAN,
description: "Enable delete on click",
default: true
},
enableDoubleClickToEdit: {
type: OptionType.BOOLEAN,
description: "Enable double click to edit",
default: true
}
},
settings,
start() {
const MessageActions = findByPropsLazy("deleteMessage", "startEditMessage");
@ -54,15 +67,30 @@ export default definePlugin({
document.addEventListener("keydown", keydown);
document.addEventListener("keyup", keyup);
this.onClick = addClickListener((msg, chan, event) => {
this.onClick = addClickListener((msg, channel, event) => {
const isMe = msg.author.id === UserStore.getCurrentUser().id;
if (!isDeletePressed) {
if (Vencord.Settings.plugins.MessageClickActions.enableDoubleClickToEdit && (isMe && event.detail >= 2 && !EditStore.isEditing(chan.id, msg.id))) {
MessageActions.startEditMessage(chan.id, msg.id, msg.content);
if (event.detail < 2) return;
if (settings.store.requireModifier && !event.ctrlKey && !event.shiftKey) return;
if (isMe) {
if (!settings.store.enableDoubleClickToEdit || EditStore.isEditing(channel.id, msg.id)) return;
MessageActions.startEditMessage(channel.id, msg.id, msg.content);
event.preventDefault();
} else {
if (!settings.store.enableDoubleClickToReply) return;
FluxDispatcher.dispatch({
type: "CREATE_PENDING_REPLY",
channel,
message: msg,
shouldMention: !Settings.plugins.NoReplyMention.enabled,
showMentionToggle: channel.guild_id !== null
});
}
} else if (Vencord.Settings.plugins.MessageClickActions.enableDeleteOnClick && (isMe || PermissionStore.can(MANAGE_CHANNELS, chan))) {
MessageActions.deleteMessage(chan.id, msg.id);
} else if (settings.store.enableDeleteOnClick && (isMe || PermissionStore.can(MANAGE_CHANNELS, channel))) {
MessageActions.deleteMessage(channel.id, msg.id);
event.preventDefault();
}
});

View File

@ -30,7 +30,7 @@ import { User } from "discord-types/general";
const SessionsStore = findStoreLazy("SessionsStore");
function Icon(path: string, viewBox = "0 0 24 24") {
return ({ color, tooltip, wantMargin }: { color: string; tooltip: string; wantMargin: boolean; }) => (
return ({ color, tooltip }: { color: string; tooltip: string; }) => (
<Tooltip text={tooltip} >
{(tooltipProps: any) => (
<svg
@ -39,12 +39,6 @@ function Icon(path: string, viewBox = "0 0 24 24") {
width="20"
viewBox={viewBox}
fill={color}
style={{
marginLeft: wantMargin ? 4 : 0,
verticalAlign: "top",
position: "relative",
top: wantMargin ? 1 : 0,
}}
>
<path d={path} />
</svg>
@ -63,11 +57,11 @@ type Platform = keyof typeof Icons;
const getStatusColor = findByCodeLazy(".TWITCH", ".STREAMING", ".INVISIBLE");
const PlatformIcon = ({ platform, status, wantMargin }: { platform: Platform, status: string; wantMargin: boolean; }) => {
const PlatformIcon = ({ platform, status }: { platform: Platform, status: string; }) => {
const tooltip = platform[0].toUpperCase() + platform.slice(1);
const Icon = Icons[platform] ?? Icons.desktop;
return <Icon color={`var(--${getStatusColor(status)}`} tooltip={tooltip} wantMargin={wantMargin} />;
return <Icon color={`var(--${getStatusColor(status)}`} tooltip={tooltip} />;
};
const getStatus = (id: string): Record<Platform, string> => PresenceStore.getState()?.clientStatuses?.[id];
@ -105,14 +99,26 @@ const PlatformIndicator = ({ user, wantMargin = true }: { user: User; wantMargin
key={platform}
platform={platform as Platform}
status={status}
wantMargin={wantMargin}
/>
));
if (!icons.length) return null;
return (
<span className="vc-platform-indicator">
<span
className="vc-platform-indicator"
style={{
display: "inline-flex",
justifyContent: "center",
marginLeft: wantMargin ? 4 : 0,
verticalAlign: "top",
position: "relative",
top: wantMargin ? 1 : 0,
padding: !wantMargin ? 2 : 0,
gap: 4
}}
>
{icons}
</span>
);

View File

@ -76,6 +76,8 @@ export default definePlugin({
name: "ReverseImageSearch",
description: "Adds ImageSearch to image context menus",
authors: [Devs.Ven, Devs.Nuckyz],
tags: ["ImageUtilities"],
patches: [
{
find: ".Messages.MESSAGE_ACTIONS_MENU_LABEL",

View File

@ -18,17 +18,13 @@
import { addContextMenuPatch } from "@api/ContextMenu";
import { Settings } from "@api/Settings";
import PatchHelper from "@components/PatchHelper";
import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import { LazyComponent } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types";
import { SettingsRouter } from "@webpack/common";
import { React, SettingsRouter } from "@webpack/common";
import gitHash from "~git-hash";
const SettingsComponent = LazyComponent(() => require("../components/VencordSettings").default);
export default definePlugin({
name: "Settings",
description: "Adds Settings UI and debug info",
@ -95,37 +91,37 @@ export default definePlugin({
{
section: "VencordSettings",
label: "Vencord",
element: () => <SettingsComponent tab="VencordSettings" />
element: require("@components/VencordSettings/VencordTab").default
},
{
section: "VencordPlugins",
label: "Plugins",
element: () => <SettingsComponent tab="VencordPlugins" />,
element: require("@components/VencordSettings/PluginsTab").default,
},
{
section: "VencordThemes",
label: "Themes",
element: () => <SettingsComponent tab="VencordThemes" />,
element: require("@components/VencordSettings/ThemesTab").default,
},
!IS_WEB && {
section: "VencordUpdater",
label: "Updater",
element: () => <SettingsComponent tab="VencordUpdater" />,
element: require("@components/VencordSettings/UpdaterTab").default,
},
{
section: "VencordCloud",
label: "Cloud",
element: () => <SettingsComponent tab="VencordCloud" />,
element: require("@components/VencordSettings/CloudTab").default,
},
{
section: "VencordSettingsSync",
label: "Backup & Restore",
element: () => <SettingsComponent tab="VencordSettingsSync" />,
element: require("@components/VencordSettings/BackupAndRestoreTab").default,
},
IS_DEV && {
section: "VencordPatchHelper",
label: "Patch Helper",
element: PatchHelper!,
element: require("@components/VencordSettings/PatchHelperTab").default,
},
IS_VENCORD_DESKTOP && {
section: "VencordDesktop",

View File

@ -371,7 +371,7 @@ export default definePlugin({
},
{
// Remove the open chat button for the HiddenChannelLockScreen
match: /"recents".+?null,(?=.{0,120}?channelId:(\i)\.id)/,
match: /"recents".+?null,(?=.+?channelId:(\i)\.id,showRequestToSpeakSidebar)/,
replace: (m, channel) => `${m}!$self.isHiddenChannel(${channel})&&`
}
],

View File

@ -33,6 +33,11 @@ const settings = definePluginSettings({
onChange(newValue: boolean) {
if (newValue === false) lastState = false;
}
},
autoDisable: {
type: OptionType.BOOLEAN,
description: "Automatically disable the silent message toggle again after sending one",
default: true
}
});
@ -51,7 +56,7 @@ function SilentMessageToggle(chatBoxProps: {
React.useEffect(() => {
const listener: SendListener = (_, message) => {
if (enabled) {
setEnabledValue(false);
if (settings.store.autoDisable) setEnabledValue(false);
if (!message.content.startsWith("@silent ")) message.content = "@silent " + message.content;
}
};
@ -96,7 +101,7 @@ function SilentMessageToggle(chatBoxProps: {
export default definePlugin({
name: "SilentMessageToggle",
authors: [Devs.Nuckyz],
authors: [Devs.Nuckyz, Devs.CatNoir],
description: "Adds a button to the chat bar to toggle sending a silent message.",
dependencies: ["MessageEventsAPI"],

View File

@ -159,7 +159,7 @@ function TextReplace({ title, rulesArray, rulesKey, update }: TextReplaceProps)
<Input
placeholder="Replace"
initialValue={rule.replace}
onChange={e => onChange(e.replaceAll("\\n", "\n"), index, "replace")}
onChange={e => onChange(e, index, "replace")}
/>
<Input
placeholder="Only if includes"
@ -220,7 +220,7 @@ function applyRules(content: string): string {
if (!rule.find || !rule.replace) continue;
if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue;
content = content.replaceAll(rule.find, rule.replace);
content = content.replaceAll(rule.find, rule.replace.replaceAll("\\n", "\n"));
}
}
@ -231,7 +231,7 @@ function applyRules(content: string): string {
try {
const regex = stringToRegex(rule.find);
content = content.replace(regex, rule.replace);
content = content.replace(regex, rule.replace.replaceAll("\\n", "\n"));
} catch (e) {
new Logger("TextReplace").error(`Invalid regex: ${rule.find}`);
}

View File

@ -43,6 +43,8 @@ export function TranslateChatBarIcon({ slateProps }: { slateProps: { type: { ana
if (slateProps.type.analyticsName !== "normal")
return null;
const toggle = () => settings.store.autoTranslate = !autoTranslate;
return (
<Tooltip text="Open Translate Modal">
{({ onMouseEnter, onMouseLeave }) => (
@ -55,11 +57,14 @@ export function TranslateChatBarIcon({ slateProps }: { slateProps: { type: { ana
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
innerClassName={ButtonWrapperClasses.button}
onClick={() =>
onClick={e => {
if (e.shiftKey) return toggle();
openModal(props => (
<TranslateModal rootProps={props} />
))
}
));
}}
onContextMenu={() => toggle()}
style={{ padding: "0 4px" }}
>
<div className={ButtonWrapperClasses.buttonWrapper}>

View File

@ -46,7 +46,7 @@ export const settings = definePluginSettings({
},
autoTranslate: {
type: OptionType.BOOLEAN,
description: "Automatically translate your messages before sending",
description: "Automatically translate your messages before sending. You can also shift/right click the translate button to toggle this",
default: false
}
});

View File

@ -47,8 +47,8 @@ const settings = definePluginSettings({
export default definePlugin({
name: "USRBG",
description: "USRBG is a community maintained database of Discord banners, allowing anyone to get a banner without requiring Nitro",
authors: [Devs.AutumnVN, Devs.pylix],
description: "Displays user banners from USRBG, allowing anyone to get a banner without Nitro",
authors: [Devs.AutumnVN, Devs.pylix, Devs.TheKodeToad],
settings,
patches: [
{
@ -61,6 +61,10 @@ export default definePlugin({
{
match: /(\i)\.bannerSrc,/,
replace: "$self.useBannerHook($1),"
},
{
match: /\?\(0,\i\.jsx\)\(\i,{type:\i,shown/,
replace: "&&$self.shouldShowBadge(arguments[0])$&"
}
]
},
@ -104,6 +108,10 @@ export default definePlugin({
if (data[userId]) return 2;
},
shouldShowBadge({ displayProfile, user }: any) {
return displayProfile?.banner && (!data[user.id] || settings.store.nitroFirst);
},
async start() {
enableStyle(style);

View File

@ -116,6 +116,7 @@ export default definePlugin({
name: "ValidUser",
description: "Fix mentions for unknown users showing up as '<@343383572805058560>' (hover over a mention to fix it)",
authors: [Devs.Ven],
tags: ["MentionCacheFix"],
patches: [{
find: 'className:"mention"',

View File

@ -84,7 +84,7 @@ function openImage(url: string) {
const UserContext: NavContextMenuPatchCallback = (children, { user, guildId }: UserContextProps) => () => {
const memberAvatar = GuildMemberStore.getMember(guildId!, user.id)?.avatar || null;
children.splice(1, 0, (
children.splice(-1, 0, (
<Menu.MenuGroup>
<Menu.MenuItem
id="view-avatar"
@ -109,13 +109,7 @@ const UserContext: NavContextMenuPatchCallback = (children, { user, guildId }: U
const GuildContext: NavContextMenuPatchCallback = (children, { guild: { id, icon, banner } }: GuildContextProps) => () => {
if (!banner && !icon) return;
// before copy id (if it exists)
const idx = children.length +
children[children.length - 1]?.props?.children?.props?.id === "devmode-copy-id"
? -2
: -1;
children.splice(idx, 0, (
children.splice(-1, 0, (
<Menu.MenuGroup>
{icon ? (
<Menu.MenuItem
@ -151,6 +145,7 @@ export default definePlugin({
name: "ViewIcons",
authors: [Devs.Ven, Devs.TheKodeToad, Devs.Nuckyz],
description: "Makes avatars and banners in user profiles clickable, and adds View Icon/Banner entries in the user and server context menu",
tags: ["ImageUtilities"],
settings,

View File

@ -286,5 +286,9 @@ export const Devs = /* #__PURE__*/ Object.freeze({
carince: {
name: "carince",
id: 818323528755314698n
},
CatNoir: {
name: "CatNoir",
id: 260371016348336128n
}
});