@@ -82,32 +72,44 @@ function ContributorModal({ user }: { user: User; }) {
/>
{user.username}
-
+
{website && (
-
-
-
+
)}
{githubName && (
-
-
-
+
)}
-
- {plugins.map(p =>
-
showToast("Restart to apply changes!")}
- />
- )}
-
+ {plugins.length ? (
+
+ This person has {ContributedHyperLink} to {pluralise(plugins.length, "plugin")}!
+
+ ) : (
+
+ This person has not made any plugins. They likely {ContributedHyperLink} to Vencord in other ways!
+
+ )}
+
+ {!!plugins.length && (
+
+ {plugins.map(p =>
+
showToast("Restart to apply changes!")}
+ />
+ )}
+
+ )}
>
);
}
diff --git a/src/components/PluginSettings/LinkIconButton.css b/src/components/PluginSettings/LinkIconButton.css
new file mode 100644
index 000000000..1055d6c70
--- /dev/null
+++ b/src/components/PluginSettings/LinkIconButton.css
@@ -0,0 +1,12 @@
+.vc-settings-modal-link-icon {
+ height: 32px;
+ width: 32px;
+ border-radius: 50%;
+ border: 4px solid var(--background-tertiary);
+ box-sizing: border-box
+}
+
+.vc-settings-modal-links {
+ display: flex;
+ gap: 0.2em;
+}
diff --git a/src/components/PluginSettings/LinkIconButton.tsx b/src/components/PluginSettings/LinkIconButton.tsx
new file mode 100644
index 000000000..ea36dda24
--- /dev/null
+++ b/src/components/PluginSettings/LinkIconButton.tsx
@@ -0,0 +1,45 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import "./LinkIconButton.css";
+
+import { getTheme, Theme } from "@utils/discord";
+import { MaskedLink, Tooltip } from "@webpack/common";
+
+const WebsiteIconDark = "/assets/e1e96d89e192de1997f73730db26e94f.svg";
+const WebsiteIconLight = "/assets/730f58bcfd5a57a5e22460c445a0c6cf.svg";
+const GithubIconLight = "/assets/3ff98ad75ac94fa883af5ed62d17c459.svg";
+const GithubIconDark = "/assets/6a853b4c87fce386cbfef4a2efbacb09.svg";
+
+export function GithubIcon() {
+ const src = getTheme() === Theme.Light ? GithubIconLight : GithubIconDark;
+ return
;
+}
+
+export function WebsiteIcon() {
+ const src = getTheme() === Theme.Light ? WebsiteIconLight : WebsiteIconDark;
+ return
;
+}
+
+interface Props {
+ text: string;
+ href: string;
+}
+
+function LinkIcon({ text, href, Icon }: Props & { Icon: React.ComponentType; }) {
+ return (
+
+ {props => (
+
+
+
+ )}
+
+ );
+}
+
+export const WebsiteButton = (props: Props) =>
;
+export const GithubButton = (props: Props) =>
;
diff --git a/src/components/PluginSettings/PluginModal.css b/src/components/PluginSettings/PluginModal.css
new file mode 100644
index 000000000..1f4b9aaad
--- /dev/null
+++ b/src/components/PluginSettings/PluginModal.css
@@ -0,0 +1,7 @@
+.vc-plugin-modal-info {
+ align-items: center;
+}
+
+.vc-plugin-modal-description {
+ flex-grow: 1;
+}
diff --git a/src/components/PluginSettings/PluginModal.tsx b/src/components/PluginSettings/PluginModal.tsx
index 34de43c2d..e5da01f36 100644
--- a/src/components/PluginSettings/PluginModal.tsx
+++ b/src/components/PluginSettings/PluginModal.tsx
@@ -16,10 +16,14 @@
* along with this program. If not, see
.
*/
+import "./PluginModal.css";
+
import { generateId } from "@api/Commands";
import { useSettings } from "@api/Settings";
+import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
+import { gitRemote } from "@shared/vencordUserAgent";
import { proxyLazy } from "@utils/lazy";
import { Margins } from "@utils/margins";
import { classes, isObjectEmpty } from "@utils/misc";
@@ -30,6 +34,8 @@ import { Button, Clickable, FluxDispatcher, Forms, React, Text, Tooltip, UserSto
import { User } from "discord-types/general";
import { Constructor } from "type-fest";
+import { PluginMeta } from "~plugins";
+
import {
ISettingElementProps,
SettingBooleanComponent,
@@ -40,6 +46,9 @@ import {
SettingTextComponent
} from "./components";
import { openContributorModal } from "./ContributorModal";
+import { GithubButton, WebsiteButton } from "./LinkIconButton";
+
+const cl = classNameFactory("vc-plugin-modal-");
const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers");
const AvatarStyles = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar");
@@ -180,16 +189,54 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
);
}
+ /*
+ function switchToPopout() {
+ onClose();
+
+ const PopoutKey = `DISCORD_VENCORD_PLUGIN_SETTINGS_MODAL_${plugin.name}`;
+ PopoutActions.open(
+ PopoutKey,
+ () =>
PopoutActions.close(PopoutKey)}
+ />
+ );
+ }
+ */
+
+ const pluginMeta = PluginMeta[plugin.name];
+
return (
{plugin.name}
+
+ {/*
+
+
+
+ */}
- About {plugin.name}
- {plugin.description}
+
+ {plugin.description}
+ {!pluginMeta.userPlugin && (
+
+
+
+
+ )}
+
Authors
require("../../plugins"));
const cl = classNameFactory("vc-plugins-");
const logger = new Logger("PluginSettings", "#a6d189");
@@ -68,7 +69,7 @@ function ReloadRequiredCard({ required }: { required: boolean; }) {
Restart now to apply new plugins and their settings
- location.reload()}>
+ location.reload()}>
Restart
>
@@ -176,6 +177,37 @@ const enum SearchStatus {
NEW
}
+function ExcludedPluginsList({ search }: { search: string; }) {
+ const matchingExcludedPlugins = Object.entries(ExcludedPlugins)
+ .filter(([name]) => name.toLowerCase().includes(search));
+
+ const ExcludedReasons: Record<"web" | "discordDesktop" | "vencordDesktop" | "desktop" | "dev", string> = {
+ desktop: "Discord Desktop app or Vesktop",
+ discordDesktop: "Discord Desktop app",
+ vencordDesktop: "Vesktop app",
+ web: "Vesktop app and the Web version of Discord",
+ dev: "Developer version of Vencord"
+ };
+
+ return (
+
+ {matchingExcludedPlugins.length
+ ? <>
+ Are you looking for:
+
+ {matchingExcludedPlugins.map(([name, reason]) => (
+
+ {name} : Only available on the {ExcludedReasons[reason]}
+
+ ))}
+
+ >
+ : "No plugins meet the search criteria."
+ }
+
+ );
+}
+
export default function PluginSettings() {
const settings = useSettings();
const changes = React.useMemo(() => new ChangeList(), []);
@@ -214,26 +246,27 @@ export default function PluginSettings() {
return o;
}, []);
- const sortedPlugins = React.useMemo(() => Object.values(Plugins)
+ const sortedPlugins = useMemo(() => Object.values(Plugins)
.sort((a, b) => a.name.localeCompare(b.name)), []);
const [searchValue, setSearchValue] = React.useState({ value: "", status: SearchStatus.ALL });
+ const search = searchValue.value.toLowerCase();
const onSearch = (query: string) => setSearchValue(prev => ({ ...prev, value: query }));
const onStatusChange = (status: SearchStatus) => setSearchValue(prev => ({ ...prev, status }));
const pluginFilter = (plugin: typeof Plugins[keyof typeof Plugins]) => {
- const enabled = settings.plugins[plugin.name]?.enabled;
- if (enabled && searchValue.status === SearchStatus.DISABLED) return false;
- if (!enabled && searchValue.status === SearchStatus.ENABLED) return false;
- if (searchValue.status === SearchStatus.NEW && !newPlugins?.includes(plugin.name)) return false;
- if (!searchValue.value.length) return true;
+ const { status } = searchValue;
+ const enabled = Vencord.Plugins.isPluginEnabled(plugin.name);
+ if (enabled && status === SearchStatus.DISABLED) return false;
+ if (!enabled && status === SearchStatus.ENABLED) return false;
+ if (status === SearchStatus.NEW && !newPlugins?.includes(plugin.name)) return false;
+ if (!search.length) return true;
- const v = searchValue.value.toLowerCase();
return (
- plugin.name.toLowerCase().includes(v) ||
- plugin.description.toLowerCase().includes(v) ||
- plugin.tags?.some(t => t.toLowerCase().includes(v))
+ plugin.name.toLowerCase().includes(search) ||
+ plugin.description.toLowerCase().includes(search) ||
+ plugin.tags?.some(t => t.toLowerCase().includes(search))
);
};
@@ -254,53 +287,48 @@ export default function PluginSettings() {
return lodash.isEqual(newPlugins, sortedPluginNames) ? [] : newPlugins;
}));
- type P = JSX.Element | JSX.Element[];
- let plugins: P, requiredPlugins: P;
- if (sortedPlugins?.length) {
- plugins = [];
- requiredPlugins = [];
+ const plugins = [] as JSX.Element[];
+ const requiredPlugins = [] as JSX.Element[];
- for (const p of sortedPlugins) {
- if (!p.options && p.name.endsWith("API") && searchValue.value !== "API")
- continue;
+ const showApi = searchValue.value.includes("API");
+ for (const p of sortedPlugins) {
+ if (p.hidden || (!p.options && p.name.endsWith("API") && !showApi))
+ continue;
- if (!pluginFilter(p)) continue;
+ if (!pluginFilter(p)) continue;
- const isRequired = p.required || depMap[p.name]?.some(d => settings.plugins[d].enabled);
+ const isRequired = p.required || depMap[p.name]?.some(d => settings.plugins[d].enabled);
- if (isRequired) {
- const tooltipText = p.required
- ? "This plugin is required for Vencord to function."
- : makeDependencyList(depMap[p.name]?.filter(d => settings.plugins[d].enabled));
-
- requiredPlugins.push(
-
- {({ onMouseLeave, onMouseEnter }) => (
- changes.handleChange(name)}
- disabled={true}
- plugin={p}
- />
- )}
-
- );
- } else {
- plugins.push(
- changes.handleChange(name)}
- disabled={false}
- plugin={p}
- isNew={newPlugins?.includes(p.name)}
- key={p.name}
- />
- );
- }
+ if (isRequired) {
+ const tooltipText = p.required
+ ? "This plugin is required for Vencord to function."
+ : makeDependencyList(depMap[p.name]?.filter(d => settings.plugins[d].enabled));
+ requiredPlugins.push(
+
+ {({ onMouseLeave, onMouseEnter }) => (
+ changes.handleChange(name)}
+ disabled={true}
+ plugin={p}
+ key={p.name}
+ />
+ )}
+
+ );
+ } else {
+ plugins.push(
+ changes.handleChange(name)}
+ disabled={false}
+ plugin={p}
+ isNew={newPlugins?.includes(p.name)}
+ key={p.name}
+ />
+ );
}
- } else {
- plugins = requiredPlugins = No plugins meet search criteria. ;
}
return (
@@ -315,7 +343,6 @@ export default function PluginSettings() {
Plugins
-
- {plugins}
-
+ {plugins.length || requiredPlugins.length
+ ? (
+
+ {plugins.length
+ ? plugins
+ : No plugins meet the search criteria.
+ }
+
+ )
+ :
+ }
+
@@ -342,7 +378,10 @@ export default function PluginSettings() {
Required Plugins
- {requiredPlugins}
+ {requiredPlugins.length
+ ? requiredPlugins
+ : No plugins meet the search criteria.
+ }
);
diff --git a/src/components/PluginSettings/styles.css b/src/components/PluginSettings/styles.css
index 66b2a2158..d3d182e58 100644
--- a/src/components/PluginSettings/styles.css
+++ b/src/components/PluginSettings/styles.css
@@ -78,6 +78,7 @@
.vc-plugins-restart-card button {
margin-top: 0.5em;
+ background: var(--info-warning-foreground) !important;
}
.vc-plugins-info-button svg:not(:hover, :focus) {
diff --git a/src/components/VencordSettings/AddonCard.tsx b/src/components/VencordSettings/AddonCard.tsx
index c4c3aaca9..1161a6411 100644
--- a/src/components/VencordSettings/AddonCard.tsx
+++ b/src/components/VencordSettings/AddonCard.tsx
@@ -21,7 +21,7 @@ import "./addonCard.css";
import { classNameFactory } from "@api/Styles";
import { Badge } from "@components/Badge";
import { Switch } from "@components/Switch";
-import { Text } from "@webpack/common";
+import { Text, useRef } from "@webpack/common";
import type { MouseEventHandler, ReactNode } from "react";
const cl = classNameFactory("vc-addon-");
@@ -42,6 +42,8 @@ interface Props {
}
export function AddonCard({ disabled, isNew, name, infoButton, footer, author, enabled, setEnabled, description, onMouseEnter, onMouseLeave }: Props) {
+ const titleRef = useRef(null);
+ const titleContainerRef = useRef(null);
return (
- {name}{isNew && }
+
+
{
+ const title = titleRef.current!;
+ const titleContainer = titleContainerRef.current!;
+
+ title.style.setProperty("--offset", `${titleContainer.clientWidth - title.scrollWidth}px`);
+ title.style.setProperty("--duration", `${Math.max(0.5, (title.scrollWidth - titleContainer.clientWidth) / 7)}s`);
+ }}
+ >
+ {name}
+
+
{isNew && }
{!!author && (
diff --git a/src/components/VencordSettings/PatchHelperTab.tsx b/src/components/VencordSettings/PatchHelperTab.tsx
index 064c872ab..e09a1dbf3 100644
--- a/src/components/VencordSettings/PatchHelperTab.tsx
+++ b/src/components/VencordSettings/PatchHelperTab.tsx
@@ -16,7 +16,6 @@
* along with this program. If not, see .
*/
-import { CheckedTextInput } from "@components/CheckedTextInput";
import { CodeBlock } from "@components/CodeBlock";
import { debounce } from "@shared/debounce";
import { Margins } from "@utils/margins";
@@ -47,7 +46,7 @@ const findCandidates = debounce(function ({ find, setModule, setError }) {
interface ReplacementComponentProps {
module: [id: number, factory: Function];
- match: string | RegExp;
+ match: string;
replacement: string | ReplaceFn;
setReplacementError(error: any): void;
}
@@ -58,7 +57,13 @@ function ReplacementComponent({ module, match, replacement, setReplacementError
const [patchedCode, matchResult, diff] = React.useMemo(() => {
const src: string = fact.toString().replaceAll("\n", "");
- const canonicalMatch = canonicalizeMatch(match);
+
+ try {
+ new RegExp(match);
+ } catch (e) {
+ return ["", [], []];
+ }
+ const canonicalMatch = canonicalizeMatch(new RegExp(match));
try {
const canonicalReplace = canonicalizeReplace(replacement, "YourPlugin");
var patched = src.replace(canonicalMatch, canonicalReplace as string);
@@ -180,7 +185,8 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
return (
<>
- replacement
+ {/* FormTitle adds a class if className is not set, so we set it to an empty string to prevent that */}
+ replacement
{!isFunc && (
-
Cheat Sheet
+
Cheat Sheet
{Object.entries({
"\\i": "Special regex escape sequence that matches identifiers (varnames, classnames, etc.)",
"$$": "Insert a $",
@@ -220,11 +226,12 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
interface FullPatchInputProps {
setFind(v: string): void;
+ setParsedFind(v: string | RegExp): void;
setMatch(v: string): void;
setReplacement(v: string | ReplaceFn): void;
}
-function FullPatchInput({ setFind, setMatch, setReplacement }: FullPatchInputProps) {
+function FullPatchInput({ setFind, setParsedFind, setMatch, setReplacement }: FullPatchInputProps) {
const [fullPatch, setFullPatch] = React.useState
("");
const [fullPatchError, setFullPatchError] = React.useState("");
@@ -233,6 +240,7 @@ function FullPatchInput({ setFind, setMatch, setReplacement }: FullPatchInputPro
setFullPatchError("");
setFind("");
+ setParsedFind("");
setMatch("");
setReplacement("");
return;
@@ -256,7 +264,8 @@ function FullPatchInput({ setFind, setMatch, setReplacement }: FullPatchInputPro
if (!parsed.replacement.match) throw new Error("No 'replacement.match' field");
if (!parsed.replacement.replace) throw new Error("No 'replacement.replace' field");
- setFind(parsed.find);
+ setFind(parsed.find instanceof RegExp ? parsed.find.toString() : parsed.find);
+ setParsedFind(parsed.find);
setMatch(parsed.replacement.match instanceof RegExp ? parsed.replacement.match.source : parsed.replacement.match);
setReplacement(parsed.replacement.replace);
setFullPatchError("");
@@ -266,7 +275,7 @@ function FullPatchInput({ setFind, setMatch, setReplacement }: FullPatchInputPro
}
return <>
- Paste your full JSON patch here to fill out the fields
+ Paste your full JSON patch here to fill out the fields
{fullPatchError !== "" && {fullPatchError} }
>;
@@ -274,6 +283,7 @@ function FullPatchInput({ setFind, setMatch, setReplacement }: FullPatchInputPro
function PatchHelper() {
const [find, setFind] = React.useState("");
+ const [parsedFind, setParsedFind] = React.useState("");
const [match, setMatch] = React.useState("");
const [replacement, setReplacement] = React.useState("");
@@ -281,34 +291,46 @@ function PatchHelper() {
const [module, setModule] = React.useState<[number, Function]>();
const [findError, setFindError] = React.useState();
+ const [matchError, setMatchError] = React.useState();
const code = React.useMemo(() => {
return `
{
- find: ${JSON.stringify(find)},
+ find: ${parsedFind instanceof RegExp ? parsedFind.toString() : JSON.stringify(parsedFind)},
replacement: {
match: /${match.replace(/(?full patch
- find
+ find
- match
- match
+ {
- try {
- return (new RegExp(v), true);
- } catch (e) {
- return (e as Error).message;
- }
- }}
+ error={matchError}
/>
+
diff --git a/src/components/VencordSettings/UpdaterTab.tsx b/src/components/VencordSettings/UpdaterTab.tsx
index 0a5d1f149..680e262d8 100644
--- a/src/components/VencordSettings/UpdaterTab.tsx
+++ b/src/components/VencordSettings/UpdaterTab.tsx
@@ -22,6 +22,7 @@ import { Flex } from "@components/Flex";
import { Link } from "@components/Link";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
+import { ModalCloseButton, ModalContent, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { relaunch } from "@utils/native";
import { useAwaiter } from "@utils/react";
import { changes, checkForUpdates, getRepo, isNewer, update, updateError, UpdateLogger } from "@utils/updater";
@@ -29,7 +30,7 @@ import { Alerts, Button, Card, Forms, Parser, React, Switch, Toasts } from "@web
import gitHash from "~git-hash";
-import { SettingsTab, wrapTab } from "./shared";
+import { handleSettingsTabError, SettingsTab, wrapTab } from "./shared";
function withDispatcher(dispatcher: React.Dispatch>, action: () => any) {
return async () => {
@@ -38,21 +39,24 @@ function withDispatcher(dispatcher: React.Dispatch
await action();
} catch (e: any) {
UpdateLogger.error("Failed to update", e);
+
+ let err: string;
if (!e) {
- var err = "An unknown error occurred (error is undefined).\nPlease try again.";
+ err = "An unknown error occurred (error is undefined).\nPlease try again.";
} else if (e.code && e.cmd) {
const { code, path, cmd, stderr } = e;
if (code === "ENOENT")
- var err = `Command \`${path}\` not found.\nPlease install it and try again`;
+ err = `Command \`${path}\` not found.\nPlease install it and try again`;
else {
- var err = `An error occurred while running \`${cmd}\`:\n`;
+ err = `An error occurred while running \`${cmd}\`:\n`;
err += stderr || `Code \`${code}\`. See the console for more info`;
}
} else {
- var err = "An unknown error occurred. See the console for more info.";
+ err = "An unknown error occurred. See the console for more info.";
}
+
Alerts.show({
title: "Oops!",
body: (
@@ -186,7 +190,7 @@ function Newer(props: CommonProps) {
}
function Updater() {
- const settings = useSettings(["notifyAboutUpdates", "autoUpdate", "autoUpdateNotification"]);
+ const settings = useSettings(["autoUpdate", "autoUpdateNotification"]);
const [repo, err, repoPending] = useAwaiter(getRepo, { fallbackValue: "Loading..." });
@@ -203,14 +207,6 @@ function Updater() {
return (
Updater Settings
- settings.notifyAboutUpdates = v}
- note="Shows a notification on startup"
- disabled={settings.autoUpdate}
- >
- Get notified about new updates
-
settings.autoUpdate = v}
@@ -253,3 +249,20 @@ function Updater() {
}
export default IS_UPDATER_DISABLED ? null : wrapTab(Updater, "Updater");
+
+export const openUpdaterModal = IS_UPDATER_DISABLED ? null : function () {
+ const UpdaterTab = wrapTab(Updater, "Updater");
+
+ try {
+ openModal(wrapTab((modalProps: ModalProps) => (
+
+
+
+
+
+
+ ), "UpdaterModal"));
+ } catch {
+ handleSettingsTabError();
+ }
+};
diff --git a/src/components/VencordSettings/addonCard.css b/src/components/VencordSettings/addonCard.css
index f2dee11d9..e46e4c29c 100644
--- a/src/components/VencordSettings/addonCard.css
+++ b/src/components/VencordSettings/addonCard.css
@@ -62,3 +62,36 @@
.vc-addon-author::before {
content: "by ";
}
+
+.vc-addon-title-container {
+ width: 100%;
+ overflow: hidden;
+ height: 1.25em;
+ position: relative;
+}
+
+.vc-addon-title {
+ position: absolute;
+ inset: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+@keyframes vc-addon-title {
+ 0% {
+ transform: translateX(0);
+ }
+
+ 50% {
+ transform: translateX(var(--offset));
+ }
+
+ 100% {
+ transform: translateX(0);
+ }
+}
+
+.vc-addon-title:hover {
+ overflow: visible;
+ animation: vc-addon-title var(--duration) linear infinite;
+}
diff --git a/src/components/VencordSettings/settingsStyles.css b/src/components/VencordSettings/settingsStyles.css
index 01cbcd557..310dba9af 100644
--- a/src/components/VencordSettings/settingsStyles.css
+++ b/src/components/VencordSettings/settingsStyles.css
@@ -65,3 +65,11 @@
/* discord also sets cursor: default which prevents the cursor from showing as text */
cursor: initial;
}
+
+.vc-updater-modal {
+ padding: 1.5em !important;
+}
+
+.vc-updater-modal-close-button {
+ float: right;
+}
diff --git a/src/components/VencordSettings/shared.tsx b/src/components/VencordSettings/shared.tsx
index 6dd34c46f..1c5f37d82 100644
--- a/src/components/VencordSettings/shared.tsx
+++ b/src/components/VencordSettings/shared.tsx
@@ -42,11 +42,11 @@ export function SettingsTab({ title, children }: PropsWithChildren<{ title: stri
);
}
-const onError = onlyOnce(handleComponentFailed);
+export const handleSettingsTabError = onlyOnce(handleComponentFailed);
-export function wrapTab(component: ComponentType, tab: string) {
+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,
+ onError: handleSettingsTabError,
});
}
diff --git a/src/components/index.ts b/src/components/index.ts
new file mode 100644
index 000000000..38e610fd8
--- /dev/null
+++ b/src/components/index.ts
@@ -0,0 +1,18 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+export * from "./Badge";
+export * from "./CheckedTextInput";
+export * from "./CodeBlock";
+export * from "./DonateButton";
+export { default as ErrorBoundary } from "./ErrorBoundary";
+export * from "./ErrorCard";
+export * from "./ExpandableHeader";
+export * from "./Flex";
+export * from "./Heart";
+export * from "./Icons";
+export * from "./Link";
+export * from "./Switch";
diff --git a/src/debug/Tracer.ts b/src/debug/Tracer.ts
index 4337e0019..7d80f425c 100644
--- a/src/debug/Tracer.ts
+++ b/src/debug/Tracer.ts
@@ -18,14 +18,14 @@
import { Logger } from "@utils/Logger";
-if (IS_DEV) {
+if (IS_DEV || IS_REPORTER) {
var traces = {} as Record;
var logger = new Logger("Tracer", "#FFD166");
}
const noop = function () { };
-export const beginTrace = !IS_DEV ? noop :
+export const beginTrace = !(IS_DEV || IS_REPORTER) ? noop :
function beginTrace(name: string, ...args: any[]) {
if (name in traces)
throw new Error(`Trace ${name} already exists!`);
@@ -33,7 +33,7 @@ export const beginTrace = !IS_DEV ? noop :
traces[name] = [performance.now(), args];
};
-export const finishTrace = !IS_DEV ? noop : function finishTrace(name: string) {
+export const finishTrace = !(IS_DEV || IS_REPORTER) ? noop : function finishTrace(name: string) {
const end = performance.now();
const [start, args] = traces[name];
@@ -48,7 +48,7 @@ type TraceNameMapper = (...args: Parameters) => string;
const noopTracer =
(name: string, f: F, mapper?: TraceNameMapper) => f;
-export const traceFunction = !IS_DEV
+export const traceFunction = !(IS_DEV || IS_REPORTER)
? noopTracer
: function traceFunction(name: string, f: F, mapper?: TraceNameMapper): F {
return function (this: any, ...args: Parameters) {
diff --git a/src/debug/loadLazyChunks.ts b/src/debug/loadLazyChunks.ts
new file mode 100644
index 000000000..64c3e0ead
--- /dev/null
+++ b/src/debug/loadLazyChunks.ts
@@ -0,0 +1,169 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { Logger } from "@utils/Logger";
+import { canonicalizeMatch } from "@utils/patches";
+import * as Webpack from "@webpack";
+import { wreq } from "@webpack";
+
+const LazyChunkLoaderLogger = new Logger("LazyChunkLoader");
+
+export async function loadLazyChunks() {
+ try {
+ LazyChunkLoaderLogger.log("Loading all chunks...");
+
+ const validChunks = new Set();
+ const invalidChunks = new Set();
+ const deferredRequires = new Set();
+
+ let chunksSearchingResolve: (value: void | PromiseLike) => void;
+ const chunksSearchingDone = new Promise(r => chunksSearchingResolve = r);
+
+ // True if resolved, false otherwise
+ const chunksSearchPromises = [] as Array<() => boolean>;
+
+ const LazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?(\i\.e\("?[^)]+?"?\)[^\]]*?)(?:\]\))?)\.then\(\i\.bind\(\i,"?([^)]+?)"?\)\)/g);
+
+ async function searchAndLoadLazyChunks(factoryCode: string) {
+ const lazyChunks = factoryCode.matchAll(LazyChunkRegex);
+ const validChunkGroups = new Set<[chunkIds: string[], entryPoint: string]>();
+
+ // Workaround for a chunk that depends on the ChannelMessage component but may be be force loaded before
+ // the chunk containing the component
+ const shouldForceDefer = factoryCode.includes(".Messages.GUILD_FEED_UNFEATURE_BUTTON_TEXT");
+
+ await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => {
+ const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Webpack.ChunkIdsRegex)).map(m => m[1]) : [];
+
+ if (chunkIds.length === 0) {
+ return;
+ }
+
+ let invalidChunkGroup = false;
+
+ for (const id of chunkIds) {
+ if (wreq.u(id) == null || wreq.u(id) === "undefined.js") continue;
+
+ const isWorkerAsset = await fetch(wreq.p + wreq.u(id))
+ .then(r => r.text())
+ .then(t => t.includes("importScripts("));
+
+ if (isWorkerAsset) {
+ invalidChunks.add(id);
+ invalidChunkGroup = true;
+ continue;
+ }
+
+ validChunks.add(id);
+ }
+
+ if (!invalidChunkGroup) {
+ validChunkGroups.add([chunkIds, entryPoint]);
+ }
+ }));
+
+ // Loads all found valid chunk groups
+ await Promise.all(
+ Array.from(validChunkGroups)
+ .map(([chunkIds]) =>
+ Promise.all(chunkIds.map(id => wreq.e(id as any).catch(() => { })))
+ )
+ );
+
+ // Requires the entry points for all valid chunk groups
+ for (const [, entryPoint] of validChunkGroups) {
+ try {
+ if (shouldForceDefer) {
+ deferredRequires.add(entryPoint);
+ continue;
+ }
+
+ if (wreq.m[entryPoint]) wreq(entryPoint as any);
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
+ // setImmediate to only check if all chunks were loaded after this function resolves
+ // We check if all chunks were loaded every time a factory is loaded
+ // If we are still looking for chunks in the other factories, the array will have that factory's chunk search promise not resolved
+ // But, if all chunk search promises are resolved, this means we found every lazy chunk loaded by Discord code and manually loaded them
+ setTimeout(() => {
+ let allResolved = true;
+
+ for (let i = 0; i < chunksSearchPromises.length; i++) {
+ const isResolved = chunksSearchPromises[i]();
+
+ if (isResolved) {
+ // Remove finished promises to avoid having to iterate through a huge array everytime
+ chunksSearchPromises.splice(i--, 1);
+ } else {
+ allResolved = false;
+ }
+ }
+
+ if (allResolved) chunksSearchingResolve();
+ }, 0);
+ }
+
+ Webpack.factoryListeners.add(factory => {
+ let isResolved = false;
+ searchAndLoadLazyChunks(factory.toString()).then(() => isResolved = true);
+
+ chunksSearchPromises.push(() => isResolved);
+ });
+
+ for (const factoryId in wreq.m) {
+ let isResolved = false;
+ searchAndLoadLazyChunks(wreq.m[factoryId].toString()).then(() => isResolved = true);
+
+ chunksSearchPromises.push(() => isResolved);
+ }
+
+ await chunksSearchingDone;
+
+ // Require deferred entry points
+ for (const deferredRequire of deferredRequires) {
+ wreq!(deferredRequire as any);
+ }
+
+ // All chunks Discord has mapped to asset files, even if they are not used anymore
+ const allChunks = [] as string[];
+
+ // Matches "id" or id:
+ for (const currentMatch of wreq!.u.toString().matchAll(/(?:"(\d+?)")|(?:(\d+?):)/g)) {
+ const id = currentMatch[1] ?? currentMatch[2];
+ if (id == null) continue;
+
+ allChunks.push(id);
+ }
+
+ if (allChunks.length === 0) throw new Error("Failed to get all chunks");
+
+ // Chunks that are not loaded (not used) by Discord code anymore
+ const chunksLeft = allChunks.filter(id => {
+ return !(validChunks.has(id) || invalidChunks.has(id));
+ });
+
+ await Promise.all(chunksLeft.map(async id => {
+ const isWorkerAsset = await fetch(wreq.p + wreq.u(id))
+ .then(r => r.text())
+ .then(t => t.includes("importScripts("));
+
+ // Loads and requires a chunk
+ if (!isWorkerAsset) {
+ await wreq.e(id as any);
+ // Technically, the id of the chunk does not match the entry point
+ // But, still try it because we have no way to get the actual entry point
+ if (wreq.m[id]) wreq(id as any);
+ }
+ }));
+
+ LazyChunkLoaderLogger.log("Finished loading all chunks!");
+ } catch (e) {
+ LazyChunkLoaderLogger.log("A fatal error occurred:", e);
+ }
+}
diff --git a/src/debug/runReporter.ts b/src/debug/runReporter.ts
new file mode 100644
index 000000000..ddd5e5f18
--- /dev/null
+++ b/src/debug/runReporter.ts
@@ -0,0 +1,84 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { Logger } from "@utils/Logger";
+import * as Webpack from "@webpack";
+import { patches } from "plugins";
+
+import { loadLazyChunks } from "./loadLazyChunks";
+
+const ReporterLogger = new Logger("Reporter");
+
+async function runReporter() {
+ try {
+ ReporterLogger.log("Starting test...");
+
+ let loadLazyChunksResolve: (value: void | PromiseLike) => void;
+ const loadLazyChunksDone = new Promise(r => loadLazyChunksResolve = r);
+
+ Webpack.beforeInitListeners.add(() => loadLazyChunks().then((loadLazyChunksResolve)));
+ await loadLazyChunksDone;
+
+ for (const patch of patches) {
+ if (!patch.all) {
+ new Logger("WebpackInterceptor").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`);
+ }
+ }
+
+ for (const [searchType, args] of Webpack.lazyWebpackSearchHistory) {
+ let method = searchType;
+
+ if (searchType === "findComponent") method = "find";
+ if (searchType === "findExportedComponent") method = "findByProps";
+ if (searchType === "waitFor" || searchType === "waitForComponent") {
+ if (typeof args[0] === "string") method = "findByProps";
+ else method = "find";
+ }
+ if (searchType === "waitForStore") method = "findStore";
+
+ let result: any;
+ try {
+ if (method === "proxyLazyWebpack" || method === "LazyComponentWebpack") {
+ const [factory] = args;
+ result = factory();
+ } else if (method === "extractAndLoadChunks") {
+ const [code, matcher] = args;
+
+ result = await Webpack.extractAndLoadChunks(code, matcher);
+ if (result === false) result = null;
+ } else if (method === "mapMangledModule") {
+ const [code, mapper] = args;
+
+ result = Webpack.mapMangledModule(code, mapper);
+ if (Object.keys(result).length !== Object.keys(mapper).length) throw new Error("Webpack Find Fail");
+ } else {
+ // @ts-ignore
+ result = Webpack[method](...args);
+ }
+
+ if (result == null || (result.$$vencordInternal != null && result.$$vencordInternal() == null)) throw new Error("Webpack Find Fail");
+ } catch (e) {
+ let logMessage = searchType;
+ if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") logMessage += `(${args[0].toString().slice(0, 147)}...)`;
+ else if (method === "extractAndLoadChunks") logMessage += `([${args[0].map(arg => `"${arg}"`).join(", ")}], ${args[1].toString()})`;
+ else if (method === "mapMangledModule") {
+ const failedMappings = Object.keys(args[1]).filter(key => result?.[key] == null);
+
+ logMessage += `("${args[0]}", {\n${failedMappings.map(mapping => `\t${mapping}: ${args[1][mapping].toString().slice(0, 147)}...`).join(",\n")}\n})`;
+ }
+ else logMessage += `(${args.map(arg => `"${arg}"`).join(", ")})`;
+
+ ReporterLogger.log("Webpack Find Fail:", logMessage);
+ }
+ }
+
+ ReporterLogger.log("Finished test");
+ } catch (e) {
+ ReporterLogger.log("A fatal error occurred:", e);
+ }
+}
+
+runReporter();
diff --git a/src/globals.d.ts b/src/globals.d.ts
index 94b5f15e8..e20ca4b71 100644
--- a/src/globals.d.ts
+++ b/src/globals.d.ts
@@ -34,9 +34,10 @@ declare global {
*/
export var IS_WEB: boolean;
export var IS_EXTENSION: boolean;
- export var IS_DEV: boolean;
export var IS_STANDALONE: boolean;
export var IS_UPDATER_DISABLED: boolean;
+ export var IS_DEV: boolean;
+ export var IS_REPORTER: boolean;
export var IS_DISCORD_DESKTOP: boolean;
export var IS_VESKTOP: boolean;
export var VERSION: string;
diff --git a/src/main/ipcMain.ts b/src/main/ipcMain.ts
index 9c9741db5..62785867e 100644
--- a/src/main/ipcMain.ts
+++ b/src/main/ipcMain.ts
@@ -23,12 +23,11 @@ import "./settings";
import { debounce } from "@shared/debounce";
import { IpcEvents } from "@shared/IpcEvents";
import { BrowserWindow, ipcMain, shell, systemPreferences } from "electron";
+import monacoHtml from "file://monacoWin.html?minify&base64";
import { FSWatcher, mkdirSync, watch, writeFileSync } from "fs";
import { open, readdir, readFile } from "fs/promises";
import { join, normalize } from "path";
-import monacoHtml from "~fileContent/monacoWin.html;base64";
-
import { getThemeInfo, stripBOM, UserThemeHeader } from "./themes";
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, THEMES_DIR } from "./utils/constants";
import { makeLinksOpenExternally } from "./utils/externalLinks";
diff --git a/src/main/monacoWin.html b/src/main/monacoWin.html
index 61d075ff2..ca7d0a78c 100644
--- a/src/main/monacoWin.html
+++ b/src/main/monacoWin.html
@@ -5,8 +5,8 @@
Vencord QuickCSS Editor
@@ -29,8 +29,8 @@
@@ -38,7 +38,7 @@