Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
16365d3ea1 | ||
|
1ec28a345b | ||
|
2fdc00b11e | ||
|
3da112680d | ||
|
1d93162036 | ||
|
7dcd32e838 | ||
|
ade31f993b | ||
|
3c7496ac6d | ||
|
63387a48ee | ||
|
3bb68467bb | ||
|
2b337eace1 | ||
|
5c5b009c41 | ||
|
0c54b1fa1d | ||
|
393f76749a | ||
|
1fe7f3c297 | ||
|
622e8dc3e0 |
7
.github/workflows/test.yml
vendored
7
.github/workflows/test.yml
vendored
@ -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
|
||||
|
@ -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!)
|
||||
|
@ -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": {
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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 (
|
||||
|
@ -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");
|
@ -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");
|
||||
|
@ -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;
|
@ -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");
|
||||
|
@ -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");
|
||||
|
@ -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");
|
@ -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");
|
||||
|
@ -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>;
|
||||
}
|
@ -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 {
|
||||
|
51
src/components/VencordSettings/shared.tsx
Normal file
51
src/components/VencordSettings/shared.tsx
Normal 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,
|
||||
});
|
||||
}
|
@ -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";
|
@ -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
|
||||
|
@ -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() {
|
||||
|
@ -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"',
|
||||
|
@ -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{}",
|
||||
}
|
||||
},
|
||||
],
|
||||
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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})&&`
|
||||
}
|
||||
],
|
||||
|
@ -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"],
|
||||
|
||||
|
@ -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}`);
|
||||
}
|
||||
|
@ -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}>
|
||||
|
@ -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
|
||||
}
|
||||
});
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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"',
|
||||
|
@ -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,
|
||||
|
||||
|
@ -286,5 +286,9 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
||||
carince: {
|
||||
name: "carince",
|
||||
id: 818323528755314698n
|
||||
},
|
||||
CatNoir: {
|
||||
name: "CatNoir",
|
||||
id: 260371016348336128n
|
||||
}
|
||||
});
|
||||
|
Reference in New Issue
Block a user