parent
ae730e8398
commit
5625d63e46
22 changed files with 945 additions and 192 deletions
|
@ -5,6 +5,7 @@ import { ErrorCard } from "./ErrorCard";
|
||||||
interface Props {
|
interface Props {
|
||||||
fallback?: React.ComponentType<React.PropsWithChildren<{ error: any; message: string; stack: string; }>>;
|
fallback?: React.ComponentType<React.PropsWithChildren<{ error: any; message: string; stack: string; }>>;
|
||||||
onError?(error: Error, errorInfo: React.ErrorInfo): void;
|
onError?(error: Error, errorInfo: React.ErrorInfo): void;
|
||||||
|
message?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const color = "#e78284";
|
const color = "#e78284";
|
||||||
|
@ -58,15 +59,14 @@ export default class ErrorBoundary extends React.Component<React.PropsWithChildr
|
||||||
{...this.state}
|
{...this.state}
|
||||||
/>;
|
/>;
|
||||||
|
|
||||||
|
const msg = this.props.message || "An error occurred while rendering this Component. More info can be found below and in your console.";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorCard style={{
|
<ErrorCard style={{
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
}}>
|
}}>
|
||||||
<h1>Oh no!</h1>
|
<h1>Oh no!</h1>
|
||||||
<p>
|
<p>{msg}</p>
|
||||||
An error occurred while rendering this Component. More info can be found below
|
|
||||||
and in your console.
|
|
||||||
</p>
|
|
||||||
<code>
|
<code>
|
||||||
{this.state.message}
|
{this.state.message}
|
||||||
{!!this.state.stack && (
|
{!!this.state.stack && (
|
||||||
|
|
|
@ -4,7 +4,7 @@ export function Flex(props: React.PropsWithChildren<{
|
||||||
flexDirection?: React.CSSProperties["flexDirection"];
|
flexDirection?: React.CSSProperties["flexDirection"];
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
className?: string;
|
className?: string;
|
||||||
}>) {
|
} & React.HTMLProps<HTMLDivElement>>) {
|
||||||
props.style ??= {};
|
props.style ??= {};
|
||||||
props.style.flexDirection ||= props.flexDirection;
|
props.style.flexDirection ||= props.flexDirection;
|
||||||
props.style.gap ??= "1em";
|
props.style.gap ??= "1em";
|
||||||
|
|
202
src/components/PluginSettings/PluginModal.tsx
Normal file
202
src/components/PluginSettings/PluginModal.tsx
Normal file
|
@ -0,0 +1,202 @@
|
||||||
|
import { User } from "discord-types/general";
|
||||||
|
import { Constructor } from "type-fest";
|
||||||
|
|
||||||
|
import { generateId } from "../../api/Commands";
|
||||||
|
import { useSettings } from "../../api/settings";
|
||||||
|
import { lazyWebpack, proxyLazy } from "../../utils";
|
||||||
|
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "../../utils/modal";
|
||||||
|
import { OptionType, Plugin } from "../../utils/types";
|
||||||
|
import { filters } from "../../webpack";
|
||||||
|
import { Button, FluxDispatcher, Forms, React, Text, Tooltip, UserStore, UserUtils } from "../../webpack/common";
|
||||||
|
import ErrorBoundary from "../ErrorBoundary";
|
||||||
|
import { Flex } from "../Flex";
|
||||||
|
import {
|
||||||
|
SettingBooleanComponent,
|
||||||
|
SettingInputComponent,
|
||||||
|
SettingNumericComponent,
|
||||||
|
SettingSelectComponent,
|
||||||
|
} from "./components";
|
||||||
|
|
||||||
|
const { FormSection, FormText, FormTitle } = Forms;
|
||||||
|
|
||||||
|
const UserSummaryItem = lazyWebpack(filters.byCode("defaultRenderUser", "showDefaultAvatarsForNullUsers"));
|
||||||
|
const AvatarStyles = lazyWebpack(filters.byProps(["moreUsers", "emptyUser", "avatarContainer", "clickableAvatar"]));
|
||||||
|
const UserRecord: Constructor<Partial<User>> = proxyLazy(() => UserStore.getCurrentUser().constructor) as any;
|
||||||
|
|
||||||
|
interface PluginModalProps extends ModalProps {
|
||||||
|
plugin: Plugin;
|
||||||
|
onRestartNeeded(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** To stop discord making unwanted requests... */
|
||||||
|
function makeDummyUser(user: { name: string, id: BigInt; }) {
|
||||||
|
const newUser = new UserRecord({
|
||||||
|
username: user.name,
|
||||||
|
id: generateId(),
|
||||||
|
bot: true,
|
||||||
|
});
|
||||||
|
FluxDispatcher.dispatch({
|
||||||
|
type: "USER_UPDATE",
|
||||||
|
user: newUser,
|
||||||
|
});
|
||||||
|
return newUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PluginModal({ plugin, onRestartNeeded, onClose, transitionState }: PluginModalProps) {
|
||||||
|
const [authors, setAuthors] = React.useState<Partial<User>[]>([]);
|
||||||
|
|
||||||
|
const pluginSettings = useSettings().plugins[plugin.name];
|
||||||
|
|
||||||
|
const [tempSettings, setTempSettings] = React.useState<Record<string, any>>({});
|
||||||
|
|
||||||
|
const [errors, setErrors] = React.useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
|
const canSubmit = () => Object.values(errors).every(e => !e);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
for (const user of plugin.authors.slice(0, 6)) {
|
||||||
|
const author = user.id ? await UserUtils.fetchUser(`${user.id}`).catch(() => null) : makeDummyUser(user);
|
||||||
|
setAuthors(a => [...a, author || makeDummyUser(user)]);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
function saveAndClose() {
|
||||||
|
if (!plugin.options) {
|
||||||
|
onClose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let restartNeeded = false;
|
||||||
|
for (const [key, value] of Object.entries(tempSettings)) {
|
||||||
|
const option = plugin.options[key];
|
||||||
|
pluginSettings[key] = value;
|
||||||
|
option?.onChange?.(value);
|
||||||
|
if (option?.restartNeeded) restartNeeded = true;
|
||||||
|
}
|
||||||
|
if (restartNeeded) onRestartNeeded();
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSettings() {
|
||||||
|
if (!pluginSettings || !plugin.options) {
|
||||||
|
return <FormText>There are no settings for this plugin.</FormText>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: JSX.Element[] = [];
|
||||||
|
for (const [key, setting] of Object.entries(plugin.options)) {
|
||||||
|
function onChange(newValue) {
|
||||||
|
setTempSettings(s => ({ ...s, [key]: newValue }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function onError(hasError: boolean) {
|
||||||
|
setErrors(e => ({ ...e, [key]: hasError }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = { onChange, pluginSettings, id: key, onError };
|
||||||
|
switch (setting.type) {
|
||||||
|
case OptionType.SELECT: {
|
||||||
|
options.push(<SettingSelectComponent key={key} option={setting} {...props} />);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case OptionType.STRING: {
|
||||||
|
options.push(<SettingInputComponent key={key} option={setting} {...props} />);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case OptionType.NUMBER:
|
||||||
|
case OptionType.BIGINT: {
|
||||||
|
options.push(<SettingNumericComponent key={key} option={setting} {...props} />);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case OptionType.BOOLEAN: {
|
||||||
|
options.push(<SettingBooleanComponent key={key} option={setting} {...props} />);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return <Flex flexDirection="column" style={{ gap: 12 }}>{options}</Flex>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMoreUsers(_label: string, count: number) {
|
||||||
|
const sliceCount = plugin.authors.length - count;
|
||||||
|
const sliceStart = plugin.authors.length - sliceCount;
|
||||||
|
const sliceEnd = sliceStart + plugin.authors.length - count;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip text={plugin.authors.slice(sliceStart, sliceEnd).map(u => u.name).join(", ")}>
|
||||||
|
{({ onMouseEnter, onMouseLeave }) => (
|
||||||
|
<div
|
||||||
|
className={AvatarStyles.moreUsers}
|
||||||
|
onMouseEnter={onMouseEnter}
|
||||||
|
onMouseLeave={onMouseLeave}
|
||||||
|
>
|
||||||
|
+{sliceCount}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalRoot transitionState={transitionState} size={ModalSize.MEDIUM}>
|
||||||
|
<ModalHeader>
|
||||||
|
<Text variant="heading-md/bold">{plugin.name}</Text>
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalContent style={{ marginBottom: 8, marginTop: 8 }}>
|
||||||
|
<FormSection>
|
||||||
|
<FormTitle tag="h3">About {plugin.name}</FormTitle>
|
||||||
|
<FormText>{plugin.description}</FormText>
|
||||||
|
<div style={{ marginTop: 8, marginBottom: 8, width: "fit-content" }}>
|
||||||
|
<UserSummaryItem
|
||||||
|
users={authors}
|
||||||
|
count={plugin.authors.length}
|
||||||
|
guildId={undefined}
|
||||||
|
renderIcon={false}
|
||||||
|
max={6}
|
||||||
|
showDefaultAvatarsForNullUsers
|
||||||
|
showUserPopout
|
||||||
|
renderMoreUsers={renderMoreUsers}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FormSection>
|
||||||
|
{!!plugin.settingsAboutComponent && (
|
||||||
|
<div style={{ marginBottom: 8 }}>
|
||||||
|
<FormSection>
|
||||||
|
<ErrorBoundary message="An error occurred while rendering this plugin's custom InfoComponent">
|
||||||
|
<plugin.settingsAboutComponent />
|
||||||
|
</ErrorBoundary>
|
||||||
|
</FormSection>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<FormSection>
|
||||||
|
<FormTitle tag="h3">Settings</FormTitle>
|
||||||
|
{renderSettings()}
|
||||||
|
</FormSection>
|
||||||
|
</ModalContent>
|
||||||
|
<ModalFooter>
|
||||||
|
<Flex>
|
||||||
|
<Button
|
||||||
|
onClick={onClose}
|
||||||
|
size={Button.Sizes.SMALL}
|
||||||
|
color={Button.Colors.RED}
|
||||||
|
>
|
||||||
|
Exit Without Saving
|
||||||
|
</Button>
|
||||||
|
<Tooltip text="You must fix all errors before saving" shouldShow={!canSubmit()}>
|
||||||
|
{({ onMouseEnter, onMouseLeave }) => (
|
||||||
|
<Button
|
||||||
|
size={Button.Sizes.SMALL}
|
||||||
|
color={Button.Colors.BRAND}
|
||||||
|
onClick={saveAndClose}
|
||||||
|
onMouseEnter={onMouseEnter}
|
||||||
|
onMouseLeave={onMouseLeave}
|
||||||
|
disabled={!canSubmit()}
|
||||||
|
>
|
||||||
|
Save & Exit
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
</Flex>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalRoot>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { ISettingElementProps } from ".";
|
||||||
|
import { PluginOptionBoolean } from "../../../utils/types";
|
||||||
|
import { Forms, React, Select } from "../../../webpack/common";
|
||||||
|
|
||||||
|
const { FormSection, FormTitle, FormText } = Forms;
|
||||||
|
|
||||||
|
export function SettingBooleanComponent({ option, pluginSettings, id, onChange, onError }: ISettingElementProps<PluginOptionBoolean>) {
|
||||||
|
const def = pluginSettings[id] ?? option.default;
|
||||||
|
|
||||||
|
const [state, setState] = React.useState(def ?? false);
|
||||||
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
onError(error !== null);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
{ label: "Enabled", value: true, default: def === true },
|
||||||
|
{ label: "Disabled", value: false, default: typeof def === "undefined" || def === false },
|
||||||
|
];
|
||||||
|
|
||||||
|
function handleChange(newValue: boolean): void {
|
||||||
|
let isValid = (option.isValid && option.isValid(newValue)) ?? true;
|
||||||
|
if (typeof isValid === "string") setError(isValid);
|
||||||
|
else if (!isValid) setError("Invalid input provided.");
|
||||||
|
else {
|
||||||
|
setError(null);
|
||||||
|
setState(newValue);
|
||||||
|
onChange(newValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormSection>
|
||||||
|
<FormTitle>{option.description}</FormTitle>
|
||||||
|
<Select
|
||||||
|
isDisabled={option.disabled?.() ?? false}
|
||||||
|
options={options}
|
||||||
|
placeholder={option.placeholder ?? "Select an option"}
|
||||||
|
maxVisibleItems={5}
|
||||||
|
closeOnSelect={true}
|
||||||
|
select={handleChange}
|
||||||
|
isSelected={v => v === state}
|
||||||
|
serialize={v => String(v)}
|
||||||
|
{...option.componentProps}
|
||||||
|
/>
|
||||||
|
{error && <FormText style={{ color: "var(--text-danger)" }}>{error}</FormText>}
|
||||||
|
</FormSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { ISettingElementProps } from ".";
|
||||||
|
import { OptionType, PluginOptionNumber } from "../../../utils/types";
|
||||||
|
import { Forms, React, TextInput } from "../../../webpack/common";
|
||||||
|
|
||||||
|
const { FormSection, FormTitle, FormText } = Forms;
|
||||||
|
|
||||||
|
const MAX_SAFE_NUMBER = BigInt(Number.MAX_SAFE_INTEGER);
|
||||||
|
|
||||||
|
export function SettingNumericComponent({ option, pluginSettings, id, onChange, onError }: ISettingElementProps<PluginOptionNumber>) {
|
||||||
|
function serialize(value: any) {
|
||||||
|
if (option.type === OptionType.BIGINT) return BigInt(value);
|
||||||
|
return Number(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [state, setState] = React.useState<any>(`${pluginSettings[id] ?? option.default ?? 0}`);
|
||||||
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
onError(error !== null);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
function handleChange(newValue) {
|
||||||
|
let isValid = (option.isValid && option.isValid(newValue)) ?? true;
|
||||||
|
if (typeof isValid === "string") setError(isValid);
|
||||||
|
else if (!isValid) setError("Invalid input provided.");
|
||||||
|
else if (option.type === OptionType.NUMBER && BigInt(newValue) >= MAX_SAFE_NUMBER) {
|
||||||
|
setState(`${Number.MAX_SAFE_INTEGER}`);
|
||||||
|
onChange(serialize(newValue));
|
||||||
|
} else {
|
||||||
|
setState(newValue);
|
||||||
|
onChange(serialize(newValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormSection>
|
||||||
|
<FormTitle>{option.description}</FormTitle>
|
||||||
|
<TextInput
|
||||||
|
type="number"
|
||||||
|
pattern="-?[0-9]+"
|
||||||
|
value={state}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder={option.placeholder ?? "Enter a number"}
|
||||||
|
disabled={option.disabled?.() ?? false}
|
||||||
|
{...option.componentProps}
|
||||||
|
/>
|
||||||
|
{error && <FormText style={{ color: "var(--text-danger)" }}>{error}</FormText>}
|
||||||
|
</FormSection>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { ISettingElementProps } from ".";
|
||||||
|
import { PluginOptionSelect } from "../../../utils/types";
|
||||||
|
import { Forms, React, Select } from "../../../webpack/common";
|
||||||
|
|
||||||
|
const { FormSection, FormTitle, FormText } = Forms;
|
||||||
|
|
||||||
|
export function SettingSelectComponent({ option, pluginSettings, onChange, onError, id }: ISettingElementProps<PluginOptionSelect>) {
|
||||||
|
const def = pluginSettings[id] ?? option.options?.find(o => o.default)?.value;
|
||||||
|
|
||||||
|
const [state, setState] = React.useState<any>(def ?? null);
|
||||||
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
onError(error !== null);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
function handleChange(newValue) {
|
||||||
|
let isValid = (option.isValid && option.isValid(newValue)) ?? true;
|
||||||
|
if (typeof isValid === "string") setError(isValid);
|
||||||
|
else if (!isValid) setError("Invalid input provided.");
|
||||||
|
else {
|
||||||
|
setState(newValue);
|
||||||
|
onChange(newValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormSection>
|
||||||
|
<FormTitle>{option.description}</FormTitle>
|
||||||
|
<Select
|
||||||
|
isDisabled={option.disabled?.() ?? false}
|
||||||
|
options={option.options}
|
||||||
|
placeholder={option.placeholder ?? "Select an option"}
|
||||||
|
maxVisibleItems={5}
|
||||||
|
closeOnSelect={true}
|
||||||
|
select={handleChange}
|
||||||
|
isSelected={v => v === state}
|
||||||
|
serialize={v => String(v)}
|
||||||
|
{...option.componentProps}
|
||||||
|
/>
|
||||||
|
{error && <FormText style={{ color: "var(--text-danger)" }}>{error}</FormText>}
|
||||||
|
</FormSection>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { ISettingElementProps } from ".";
|
||||||
|
import { PluginOptionString } from "../../../utils/types";
|
||||||
|
import { Forms, React, TextInput } from "../../../webpack/common";
|
||||||
|
|
||||||
|
const { FormSection, FormTitle, FormText } = Forms;
|
||||||
|
|
||||||
|
export function SettingInputComponent({ option, pluginSettings, id, onChange, onError }: ISettingElementProps<PluginOptionString>) {
|
||||||
|
const [state, setState] = React.useState(pluginSettings[id] ?? option.default ?? null);
|
||||||
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
onError(error !== null);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
function handleChange(newValue) {
|
||||||
|
let isValid = (option.isValid && option.isValid(newValue)) ?? true;
|
||||||
|
if (typeof isValid === "string") setError(isValid);
|
||||||
|
else if (!isValid) setError("Invalid input provided.");
|
||||||
|
else {
|
||||||
|
setState(newValue);
|
||||||
|
onChange(newValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormSection>
|
||||||
|
<FormTitle>{option.description}</FormTitle>
|
||||||
|
<TextInput
|
||||||
|
type="text"
|
||||||
|
value={state}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder={option.placeholder ?? "Enter a value"}
|
||||||
|
disabled={option.disabled?.() ?? false}
|
||||||
|
{...option.componentProps}
|
||||||
|
/>
|
||||||
|
{error && <FormText style={{ color: "var(--text-danger)" }}>{error}</FormText>}
|
||||||
|
</FormSection>
|
||||||
|
);
|
||||||
|
}
|
17
src/components/PluginSettings/components/index.ts
Normal file
17
src/components/PluginSettings/components/index.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { PluginOptionBase } from "../../../utils/types";
|
||||||
|
|
||||||
|
export interface ISettingElementProps<T extends PluginOptionBase> {
|
||||||
|
option: T;
|
||||||
|
onChange(newValue: any): void;
|
||||||
|
pluginSettings: {
|
||||||
|
[setting: string]: any;
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
|
id: string;
|
||||||
|
onError(hasError: boolean): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export * from "./SettingBooleanComponent";
|
||||||
|
export * from "./SettingNumericComponent";
|
||||||
|
export * from "./SettingSelectComponent";
|
||||||
|
export * from "./SettingTextComponent";
|
234
src/components/PluginSettings/index.tsx
Normal file
234
src/components/PluginSettings/index.tsx
Normal file
|
@ -0,0 +1,234 @@
|
||||||
|
import Plugins from "plugins";
|
||||||
|
|
||||||
|
import { Settings, useSettings } from "../../api/settings";
|
||||||
|
import { startPlugin, stopPlugin } from "../../plugins";
|
||||||
|
import { Modals } from "../../utils";
|
||||||
|
import { ChangeList } from "../../utils/ChangeList";
|
||||||
|
import { classes, lazyWebpack } from "../../utils/misc";
|
||||||
|
import { Plugin } from "../../utils/types";
|
||||||
|
import { filters } from "../../webpack";
|
||||||
|
import { Alerts, Button, Forms, Margins, Parser, React, Text, TextInput, Toasts, Tooltip } from "../../webpack/common";
|
||||||
|
import ErrorBoundary from "../ErrorBoundary";
|
||||||
|
import { Flex } from "../Flex";
|
||||||
|
import PluginModal from "./PluginModal";
|
||||||
|
import * as styles from "./styles";
|
||||||
|
|
||||||
|
const Select = lazyWebpack(filters.byCode("optionClassName", "popoutPosition", "autoFocus", "maxVisibleItems"));
|
||||||
|
const InputStyles = lazyWebpack(filters.byProps(["inputDefault", "inputWrapper"]));
|
||||||
|
|
||||||
|
function showErrorToast(message: string) {
|
||||||
|
Toasts.show({
|
||||||
|
message,
|
||||||
|
type: Toasts.Type.FAILURE,
|
||||||
|
id: Toasts.genId(),
|
||||||
|
options: {
|
||||||
|
position: Toasts.Position.BOTTOM
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PluginCardProps extends React.HTMLProps<HTMLDivElement> {
|
||||||
|
plugin: Plugin;
|
||||||
|
disabled: boolean;
|
||||||
|
onRestartNeeded(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave }: PluginCardProps) {
|
||||||
|
const settings = useSettings().plugins[plugin.name];
|
||||||
|
|
||||||
|
function isEnabled() {
|
||||||
|
return settings?.enabled || plugin.started;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openModal() {
|
||||||
|
Modals.openModalLazy(async () => {
|
||||||
|
return modalProps => {
|
||||||
|
return <PluginModal {...modalProps} plugin={plugin} onRestartNeeded={onRestartNeeded} />;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleEnabled() {
|
||||||
|
const enabled = isEnabled();
|
||||||
|
const result = enabled ? stopPlugin(plugin) : startPlugin(plugin);
|
||||||
|
const action = enabled ? "stop" : "start";
|
||||||
|
if (!result) {
|
||||||
|
showErrorToast(`Failed to ${action} plugin: ${plugin.name}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
settings.enabled = !settings.enabled;
|
||||||
|
if (plugin.patches) onRestartNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex style={styles.PluginsGridItem} flexDirection="column" onClick={() => openModal()} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
|
||||||
|
<Text variant="text-md/bold">{plugin.name}</Text>
|
||||||
|
<Text variant="text-md/normal" style={{ height: 40, overflow: "hidden" }}>{plugin.description}</Text>
|
||||||
|
<Flex flexDirection="row-reverse" style={{ marginTop: "auto", width: "100%", justifyContent: "space-between" }}>
|
||||||
|
<Button
|
||||||
|
onClick={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
toggleEnabled();
|
||||||
|
}}
|
||||||
|
disabled={disabled}
|
||||||
|
color={isEnabled() ? Button.Colors.RED : Button.Colors.GREEN}
|
||||||
|
>
|
||||||
|
{isEnabled() ? "Disable" : "Enable"}
|
||||||
|
</Button>
|
||||||
|
{plugin.options && <Forms.FormText style={{ cursor: "pointer", margin: "auto 0 auto 10px" }}>Click to configure</Forms.FormText>}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorBoundary.wrap(function Settings() {
|
||||||
|
const settings = useSettings();
|
||||||
|
const changes = React.useMemo(() => new ChangeList<string>(), []);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
return () => void (changes.hasChanges && Alerts.show({
|
||||||
|
title: "Restart required",
|
||||||
|
body: (
|
||||||
|
<>
|
||||||
|
<p>The following plugins require a restart:</p>
|
||||||
|
<div>{changes.map((s, i) => (
|
||||||
|
<>
|
||||||
|
{i > 0 && ", "}
|
||||||
|
{Parser.parse("`" + s + "`")}
|
||||||
|
</>
|
||||||
|
))}</div>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
confirmText: "Restart now",
|
||||||
|
cancelText: "Later!",
|
||||||
|
onConfirm: () => location.reload()
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const depMap = React.useMemo(() => {
|
||||||
|
const o = {} as Record<string, string[]>;
|
||||||
|
for (const plugin in Plugins) {
|
||||||
|
const deps = Plugins[plugin].dependencies;
|
||||||
|
if (deps) {
|
||||||
|
for (const dep of deps) {
|
||||||
|
o[dep] ??= [];
|
||||||
|
o[dep].push(plugin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return o;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
function hasDependents(plugin: Plugin) {
|
||||||
|
const enabledDependants = depMap[plugin.name]?.filter(d => settings.plugins[d].enabled);
|
||||||
|
return !!enabledDependants?.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedPlugins = React.useMemo(() => Object.values(Plugins)
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name)), []);
|
||||||
|
|
||||||
|
const [searchValue, setSearchValue] = React.useState({ value: "", status: "all" });
|
||||||
|
|
||||||
|
const onSearch = (query: string) => setSearchValue(prev => ({ ...prev, value: query }));
|
||||||
|
const onStatusChange = (status: string) => setSearchValue(prev => ({ ...prev, status }));
|
||||||
|
|
||||||
|
const pluginFilter = (plugin: typeof Plugins[keyof typeof Plugins]) => {
|
||||||
|
const showEnabled = searchValue.status === "enabled" || searchValue.status === "all";
|
||||||
|
const showDisabled = searchValue.status === "disabled" || searchValue.status === "all";
|
||||||
|
const enabled = settings.plugins[plugin.name]?.enabled || plugin.started;
|
||||||
|
return (
|
||||||
|
((showEnabled && enabled) || (showDisabled && !enabled)) &&
|
||||||
|
(
|
||||||
|
plugin.name.toLowerCase().includes(searchValue.value.toLowerCase()) ||
|
||||||
|
plugin.description.toLowerCase().includes(searchValue.value.toLowerCase())
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Forms.FormSection tag="h1" title="Vencord">
|
||||||
|
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
|
||||||
|
Plugins
|
||||||
|
</Forms.FormTitle>
|
||||||
|
<div style={styles.FiltersBar}>
|
||||||
|
<TextInput value={searchValue.value} placeholder={"Search for a plugin..."} onChange={onSearch} style={{ marginBottom: 24 }} />
|
||||||
|
<div className={InputStyles.inputWrapper}>
|
||||||
|
<Select
|
||||||
|
className={InputStyles.inputDefault}
|
||||||
|
options={[
|
||||||
|
{ label: "Show All", value: "all", default: true },
|
||||||
|
{ label: "Show Enabled", value: "enabled" },
|
||||||
|
{ label: "Show Disabled", value: "disabled" }
|
||||||
|
]}
|
||||||
|
serialize={v => String(v)}
|
||||||
|
select={onStatusChange}
|
||||||
|
isSelected={v => v === searchValue.status}
|
||||||
|
closeOnSelect={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={styles.PluginsGrid}>
|
||||||
|
{sortedPlugins?.length ? sortedPlugins
|
||||||
|
.filter(a => !a.required && !dependencyCheck(a.name, depMap).length && pluginFilter(a))
|
||||||
|
.map(plugin => {
|
||||||
|
const enabledDependants = depMap[plugin.name]?.filter(d => settings.plugins[d].enabled);
|
||||||
|
const dependency = enabledDependants?.length;
|
||||||
|
return <PluginCard
|
||||||
|
onRestartNeeded={() => {
|
||||||
|
changes.handleChange(plugin.name);
|
||||||
|
}}
|
||||||
|
disabled={plugin.required || !!dependency}
|
||||||
|
plugin={plugin}
|
||||||
|
/>;
|
||||||
|
})
|
||||||
|
: <Text variant="text-md/normal">No plugins meet search criteria.</Text>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<Forms.FormDivider />
|
||||||
|
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
|
||||||
|
Required Plugins
|
||||||
|
</Forms.FormTitle>
|
||||||
|
<div style={styles.PluginsGrid}>
|
||||||
|
{sortedPlugins?.length ? sortedPlugins
|
||||||
|
.filter(a => a.required || dependencyCheck(a.name, depMap).length && pluginFilter(a))
|
||||||
|
.map(plugin => {
|
||||||
|
const enabledDependants = depMap[plugin.name]?.filter(d => settings.plugins[d].enabled);
|
||||||
|
const dependency = enabledDependants?.length;
|
||||||
|
const tooltipText = plugin.required
|
||||||
|
? "This plugin is required for Vencord to function."
|
||||||
|
: makeDependencyList(dependencyCheck(plugin.name, depMap));
|
||||||
|
return <Tooltip text={tooltipText}>
|
||||||
|
{({ onMouseLeave, onMouseEnter }) => (
|
||||||
|
<PluginCard
|
||||||
|
onMouseLeave={onMouseLeave}
|
||||||
|
onMouseEnter={onMouseEnter}
|
||||||
|
onRestartNeeded={() => {
|
||||||
|
changes.handleChange(plugin.name);
|
||||||
|
}}
|
||||||
|
disabled={plugin.required || !!dependency}
|
||||||
|
plugin={plugin}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Tooltip>;
|
||||||
|
})
|
||||||
|
: <Text variant="text-md/normal">No plugins meet search criteria.</Text>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</Forms.FormSection >
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
function makeDependencyList(deps: string[]) {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Forms.FormText>This plugin is required by:</Forms.FormText>
|
||||||
|
{deps.map((dep: string) => <Forms.FormText style={{ margin: "0 auto" }}>{dep}</Forms.FormText>)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dependencyCheck(pluginName: string, depMap: Record<string, string[]>): string[] {
|
||||||
|
return depMap[pluginName] || [];
|
||||||
|
}
|
24
src/components/PluginSettings/styles.ts
Normal file
24
src/components/PluginSettings/styles.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
export const PluginsGrid: React.CSSProperties = {
|
||||||
|
marginTop: 16,
|
||||||
|
display: "grid",
|
||||||
|
gridGap: 16,
|
||||||
|
gridTemplateColumns: "repeat(auto-fill, minmax(250px, 1fr))",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PluginsGridItem: React.CSSProperties = {
|
||||||
|
backgroundColor: "var(--background-modifier-selected)",
|
||||||
|
color: "var(--interactive-active)",
|
||||||
|
borderRadius: 3,
|
||||||
|
cursor: "pointer",
|
||||||
|
display: "block",
|
||||||
|
height: 150,
|
||||||
|
padding: 10,
|
||||||
|
width: "100%",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FiltersBar: React.CSSProperties = {
|
||||||
|
gap: 10,
|
||||||
|
height: 40,
|
||||||
|
gridTemplateColumns: "1fr 150px",
|
||||||
|
display: "grid"
|
||||||
|
};
|
|
@ -1,25 +1,10 @@
|
||||||
import { classes, humanFriendlyJoin, useAwaiter } from "../utils/misc";
|
|
||||||
import Plugins from "plugins";
|
|
||||||
import { useSettings } from "../api/settings";
|
import { useSettings } from "../api/settings";
|
||||||
import IpcEvents from "../utils/IpcEvents";
|
|
||||||
|
|
||||||
import { Button, Switch, Forms, React, Margins, Toasts, Alerts, Parser } from "../webpack/common";
|
|
||||||
import ErrorBoundary from "./ErrorBoundary";
|
|
||||||
import { startPlugin } from "../plugins";
|
|
||||||
import { stopPlugin } from "../plugins/index";
|
|
||||||
import { Flex } from "./Flex";
|
|
||||||
import { ChangeList } from "../utils/ChangeList";
|
import { ChangeList } from "../utils/ChangeList";
|
||||||
|
import IpcEvents from "../utils/IpcEvents";
|
||||||
function showErrorToast(message: string) {
|
import { useAwaiter } from "../utils/misc";
|
||||||
Toasts.show({
|
import { Alerts, Button, Forms, Margins, Parser, React, Switch } from "../webpack/common";
|
||||||
message,
|
import ErrorBoundary from "./ErrorBoundary";
|
||||||
type: Toasts.Type.FAILURE,
|
import { Flex } from "./Flex";
|
||||||
id: Toasts.genId(),
|
|
||||||
options: {
|
|
||||||
position: Toasts.Position.BOTTOM
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ErrorBoundary.wrap(function Settings() {
|
export default ErrorBoundary.wrap(function Settings() {
|
||||||
const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke<string>(IpcEvents.GET_SETTINGS_DIR), "Loading...");
|
const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke<string>(IpcEvents.GET_SETTINGS_DIR), "Loading...");
|
||||||
|
@ -46,21 +31,6 @@ export default ErrorBoundary.wrap(function Settings() {
|
||||||
}));
|
}));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const depMap = React.useMemo(() => {
|
|
||||||
const o = {} as Record<string, string[]>;
|
|
||||||
for (const plugin in Plugins) {
|
|
||||||
const deps = Plugins[plugin].dependencies;
|
|
||||||
if (deps) {
|
|
||||||
for (const dep of deps) {
|
|
||||||
o[dep] ??= [];
|
|
||||||
o[dep].push(plugin);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return o;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const sortedPlugins = React.useMemo(() => Object.values(Plugins).sort((a, b) => a.name.localeCompare(b.name)), []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Forms.FormSection tag="h1" title="Vencord">
|
<Forms.FormSection tag="h1" title="Vencord">
|
||||||
|
@ -69,10 +39,10 @@ export default ErrorBoundary.wrap(function Settings() {
|
||||||
</Forms.FormTitle>
|
</Forms.FormTitle>
|
||||||
|
|
||||||
<Forms.FormText>
|
<Forms.FormText>
|
||||||
SettingsDir: <code style={{ userSelect: "text", cursor: "text" }}>{settingsDir}</code>
|
Settings Directory: <code style={{ userSelect: "text", cursor: "text" }}>{settingsDir}</code>
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
|
|
||||||
{!IS_WEB && <Flex className={Margins.marginBottom20}>
|
{!IS_WEB && <Flex className={Margins.marginBottom20} style={{ marginTop: 8 }}>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => window.DiscordNative.app.relaunch()}
|
onClick={() => window.DiscordNative.app.relaunch()}
|
||||||
size={Button.Sizes.SMALL}
|
size={Button.Sizes.SMALL}
|
||||||
|
@ -118,58 +88,6 @@ export default ErrorBoundary.wrap(function Settings() {
|
||||||
>
|
>
|
||||||
Get notified about new Updates
|
Get notified about new Updates
|
||||||
</Switch>}
|
</Switch>}
|
||||||
|
|
||||||
<Forms.FormDivider />
|
|
||||||
|
|
||||||
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
|
|
||||||
Plugins
|
|
||||||
</Forms.FormTitle>
|
|
||||||
|
|
||||||
{sortedPlugins.map(p => {
|
|
||||||
const enabledDependants = depMap[p.name]?.filter(d => settings.plugins[d].enabled);
|
|
||||||
const dependency = enabledDependants?.length;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Switch
|
|
||||||
disabled={p.required || dependency}
|
|
||||||
key={p.name}
|
|
||||||
value={settings.plugins[p.name].enabled || p.required || dependency}
|
|
||||||
onChange={(v: boolean) => {
|
|
||||||
settings.plugins[p.name].enabled = v;
|
|
||||||
let needsRestart = Boolean(p.patches?.length);
|
|
||||||
if (v) {
|
|
||||||
p.dependencies?.forEach(d => {
|
|
||||||
const dep = Plugins[d];
|
|
||||||
needsRestart ||= Boolean(dep.patches?.length && !settings.plugins[d].enabled);
|
|
||||||
settings.plugins[d].enabled = true;
|
|
||||||
if (!needsRestart && !dep.started && !startPlugin(dep)) {
|
|
||||||
showErrorToast(`Failed to start dependency ${d}. Check the console for more info.`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!needsRestart && !p.started && !startPlugin(p)) {
|
|
||||||
showErrorToast(`Failed to start plugin ${p.name}. Check the console for more info.`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if ((p.started || !p.start && p.commands?.length) && !stopPlugin(p)) {
|
|
||||||
showErrorToast(`Failed to stop plugin ${p.name}. Check the console for more info.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (needsRestart) changes.handleChange(p.name);
|
|
||||||
}}
|
|
||||||
note={p.description}
|
|
||||||
tooltipNote={
|
|
||||||
p.required ?
|
|
||||||
"This plugin is required. Thus you cannot disable it."
|
|
||||||
: dependency ?
|
|
||||||
`${humanFriendlyJoin(enabledDependants)} ${enabledDependants.length === 1 ? "depends" : "depend"} on this plugin. Thus you cannot disable it.`
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{p.name}
|
|
||||||
</Switch>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</Forms.FormSection >
|
</Forms.FormSection >
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
export { default as Settings } from "./Settings";
|
export { default as Settings } from "./Settings";
|
||||||
|
export { default as PluginSettings } from "./PluginSettings";
|
||||||
export { default as Updater } from "./Updater";
|
export { default as Updater } from "./Updater";
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
import { Devs } from "../utils/constants";
|
|
||||||
import definePlugin from "../utils/types";
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "Experiments",
|
|
||||||
authors: [Devs.Ven, Devs.Megu],
|
|
||||||
description: "Enable Experiments",
|
|
||||||
patches: [{
|
|
||||||
find: "Object.defineProperties(this,{isDeveloper",
|
|
||||||
replacement: {
|
|
||||||
match: /(?<={isDeveloper:\{[^}]+,get:function\(\)\{return )\w/,
|
|
||||||
replace: "true"
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
find: 'type:"user",revision',
|
|
||||||
replacement: {
|
|
||||||
match: /(\w)\|\|"CONNECTION_OPEN".+?;/g,
|
|
||||||
replace: "$1=!0;"
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
});
|
|
74
src/plugins/experiments.tsx
Normal file
74
src/plugins/experiments.tsx
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import { lazyWebpack } from "../utils";
|
||||||
|
import { Devs } from "../utils/constants";
|
||||||
|
import definePlugin, { OptionType } from "../utils/types";
|
||||||
|
import { Settings } from "../Vencord";
|
||||||
|
import { filters } from "../webpack";
|
||||||
|
import { Forms, React } from "../webpack/common";
|
||||||
|
|
||||||
|
const KbdStyles = lazyWebpack(filters.byProps(["key", "removeBuildOverride"]));
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "Experiments",
|
||||||
|
authors: [
|
||||||
|
Devs.Megu,
|
||||||
|
Devs.Ven,
|
||||||
|
{ name: "Nickyux", id: 427146305651998721n },
|
||||||
|
{ name: "BanTheNons", id: 460478012794863637n },
|
||||||
|
],
|
||||||
|
description: "Enable Access to Experiments in Discord!",
|
||||||
|
patches: [{
|
||||||
|
find: "Object.defineProperties(this,{isDeveloper",
|
||||||
|
replacement: {
|
||||||
|
match: /(?<={isDeveloper:\{[^}]+,get:function\(\)\{return )\w/,
|
||||||
|
replace: "true"
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
find: 'type:"user",revision',
|
||||||
|
replacement: {
|
||||||
|
match: /(\w)\|\|"CONNECTION_OPEN".+?;/g,
|
||||||
|
replace: "$1=!0;"
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
find: ".isStaff=function(){",
|
||||||
|
predicate: () => Settings.plugins["Experiments"].enableIsStaff === true,
|
||||||
|
replacement: [
|
||||||
|
{
|
||||||
|
match: /return\s*(\w+)\.hasFlag\((.+?)\.STAFF\)}/,
|
||||||
|
replace: "return Vencord.Webpack.Common.UserStore.getCurrentUser().id===$1.id||$1.hasFlag($2.STAFF)}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: /hasFreePremium=function\(\){return this.is Staff\(\)\s*\|\|/,
|
||||||
|
replace: "hasFreePremium=function(){return ",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
options: {
|
||||||
|
enableIsStaff: {
|
||||||
|
description: "Enable isStaff (requires restart)",
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
default: false,
|
||||||
|
restartNeeded: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
settingsAboutComponent: () => {
|
||||||
|
const isMacOS = navigator.platform.includes("Mac");
|
||||||
|
const modKey = isMacOS ? "cmd" : "ctrl";
|
||||||
|
const altKey = isMacOS ? "opt" : "alt";
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Forms.FormTitle tag="h3">More Information</Forms.FormTitle>
|
||||||
|
<Forms.FormText variant="text-md/normal">
|
||||||
|
You can enable client DevTools{" "}
|
||||||
|
<kbd className={KbdStyles.key}>{modKey}</kbd> +{" "}
|
||||||
|
<kbd className={KbdStyles.key}>{altKey}</kbd> +{" "}
|
||||||
|
<kbd className={KbdStyles.key}>O</kbd>{" "}
|
||||||
|
after enabling <code>isStaff</code> below
|
||||||
|
</Forms.FormText>
|
||||||
|
<Forms.FormText>
|
||||||
|
and then toggling <code>Enable DevTools</code> in the <code>Developer Options</code> tab in settings.
|
||||||
|
</Forms.FormText>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,4 +1,5 @@
|
||||||
import Plugins from "plugins";
|
import Plugins from "plugins";
|
||||||
|
|
||||||
import { registerCommand, unregisterCommand } from "../api/Commands";
|
import { registerCommand, unregisterCommand } from "../api/Commands";
|
||||||
import { Settings } from "../api/settings";
|
import { Settings } from "../api/settings";
|
||||||
import Logger from "../utils/logger";
|
import Logger from "../utils/logger";
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
import { Devs } from "../utils/constants";
|
|
||||||
import definePlugin from "../utils/types";
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "isStaff",
|
|
||||||
description:
|
|
||||||
"Gives access to client devtools & other things locked behind isStaff",
|
|
||||||
authors: [
|
|
||||||
Devs.Megu,
|
|
||||||
{
|
|
||||||
name: "Nickyux",
|
|
||||||
id: 427146305651998721n
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "BanTheNons",
|
|
||||||
id: 460478012794863637n
|
|
||||||
}
|
|
||||||
],
|
|
||||||
patches: [
|
|
||||||
{
|
|
||||||
find: ".isStaff=function(){",
|
|
||||||
replacement: [
|
|
||||||
{
|
|
||||||
match: /return\s*(\w+)\.hasFlag\((.+?)\.STAFF\)}/,
|
|
||||||
replace: "return Vencord.Webpack.Common.UserStore.getCurrentUser().id===$1.id||$1.hasFlag($2.STAFF)}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
match: /hasFreePremium=function\(\){return this.isStaff\(\)\s*\|\|/,
|
|
||||||
replace: "hasFreePremium=function(){return ",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
|
@ -1,11 +1,12 @@
|
||||||
import definePlugin from "../utils/types";
|
|
||||||
import gitHash from "git-hash";
|
import gitHash from "git-hash";
|
||||||
|
|
||||||
import { Devs } from "../utils/constants";
|
import { Devs } from "../utils/constants";
|
||||||
|
import definePlugin from "../utils/types";
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "Settings",
|
name: "Settings",
|
||||||
description: "Adds Settings UI and debug info",
|
description: "Adds Settings UI and debug info",
|
||||||
authors: [Devs.Ven],
|
authors: [Devs.Ven, Devs.Megu],
|
||||||
required: true,
|
required: true,
|
||||||
patches: [{
|
patches: [{
|
||||||
find: "().versionHash",
|
find: "().versionHash",
|
||||||
|
@ -33,6 +34,7 @@ export default definePlugin({
|
||||||
return (
|
return (
|
||||||
`{section:${mod}.ID.HEADER,label:"Vencord"},` +
|
`{section:${mod}.ID.HEADER,label:"Vencord"},` +
|
||||||
'{section:"VencordSetting",label:"Vencord",element:Vencord.Components.Settings},' +
|
'{section:"VencordSetting",label:"Vencord",element:Vencord.Components.Settings},' +
|
||||||
|
'{section:"VencordPlugins",label:"Plugins",element:Vencord.Components.PluginSettings},' +
|
||||||
updater +
|
updater +
|
||||||
`{section:${mod}.ID.DIVIDER},${m}`
|
`{section:${mod}.ID.DIVIDER},${m}`
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Devs } from "../utils/constants";
|
import { Devs } from "../utils/constants";
|
||||||
import definePlugin from "../utils/types";
|
import definePlugin from "../utils/types";
|
||||||
import { lazyWebpack, makeLazy } from "../utils/misc";
|
import { lazyWebpack, makeLazy } from "../utils/misc";
|
||||||
import { ModalSize, openModal } from "../utils/modal";
|
import { ModalRoot, ModalSize, openModal } from "../utils/modal";
|
||||||
import { find } from "../webpack";
|
import { find } from "../webpack";
|
||||||
import { React } from "../webpack/common";
|
import { React } from "../webpack/common";
|
||||||
|
|
||||||
|
@ -15,14 +15,16 @@ export default definePlugin({
|
||||||
description: "Makes Avatars/Banners in user profiles clickable, and adds Guild Context Menu Entries to View Banner/Icon.",
|
description: "Makes Avatars/Banners in user profiles clickable, and adds Guild Context Menu Entries to View Banner/Icon.",
|
||||||
|
|
||||||
openImage(url: string) {
|
openImage(url: string) {
|
||||||
openModal(() => (
|
openModal(modalProps => (
|
||||||
<ImageModal
|
<ModalRoot size={ModalSize.DYNAMIC} {...modalProps}>
|
||||||
shouldAnimate={true}
|
<ImageModal
|
||||||
original={url}
|
shouldAnimate={true}
|
||||||
src={url}
|
original={url}
|
||||||
renderLinkComponent={props => React.createElement(getMaskedLink(), props)}
|
src={url}
|
||||||
/>
|
renderLinkComponent={props => React.createElement(getMaskedLink(), props)}
|
||||||
), { size: ModalSize.DYNAMIC });
|
/>
|
||||||
|
</ModalRoot>
|
||||||
|
));
|
||||||
},
|
},
|
||||||
|
|
||||||
patches: [
|
patches: [
|
||||||
|
|
|
@ -1,15 +1,6 @@
|
||||||
import { filters } from "../webpack";
|
import { filters } from "../webpack";
|
||||||
import { lazyWebpack } from "./misc";
|
|
||||||
import { mapMangledModuleLazy } from "../webpack/webpack";
|
import { mapMangledModuleLazy } from "../webpack/webpack";
|
||||||
|
|
||||||
const ModalRoot = lazyWebpack(filters.byCode("headerIdIsManaged:"));
|
|
||||||
const Modals = mapMangledModuleLazy("onCloseRequest:null!=", {
|
|
||||||
openModal: filters.byCode("onCloseRequest:null!="),
|
|
||||||
closeModal: filters.byCode("onCloseCallback&&")
|
|
||||||
});
|
|
||||||
|
|
||||||
let modalId = 1337;
|
|
||||||
|
|
||||||
export enum ModalSize {
|
export enum ModalSize {
|
||||||
SMALL = "small",
|
SMALL = "small",
|
||||||
MEDIUM = "medium",
|
MEDIUM = "medium",
|
||||||
|
@ -17,26 +8,64 @@ export enum ModalSize {
|
||||||
DYNAMIC = "dynamic",
|
DYNAMIC = "dynamic",
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
enum ModalTransitionState {
|
||||||
* Open a modal
|
ENTERING,
|
||||||
* @param Component The component to render in the modal
|
ENTERED,
|
||||||
* @returns The key of this modal. This can be used to close the modal later with closeModal
|
EXITING,
|
||||||
*/
|
EXITED,
|
||||||
export function openModal(Component: React.ComponentType, modalProps: Record<string, any>) {
|
HIDDEN,
|
||||||
let key = `Vencord${modalId++}`;
|
|
||||||
Modals.openModal(props => (
|
|
||||||
<ModalRoot {...props} {...modalProps}>
|
|
||||||
<Component />
|
|
||||||
</ModalRoot>
|
|
||||||
), { modalKey: key });
|
|
||||||
|
|
||||||
return key;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export interface ModalProps {
|
||||||
* Close a modal by key. The id you need for this is returned by openModal.
|
transitionState: ModalTransitionState;
|
||||||
* @param key The key of the modal to close
|
onClose(): Promise<void>;
|
||||||
*/
|
}
|
||||||
export function closeModal(key: string) {
|
|
||||||
Modals.closeModal(key);
|
export interface ModalOptions {
|
||||||
|
modalKey?: string;
|
||||||
|
onCloseRequest?: (() => void);
|
||||||
|
onCloseCallback?: (() => void);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ModalRootProps {
|
||||||
|
transitionState: ModalTransitionState;
|
||||||
|
children: React.ReactNode;
|
||||||
|
size?: ModalSize;
|
||||||
|
role?: "alertdialog" | "dialog";
|
||||||
|
className?: string;
|
||||||
|
onAnimationEnd?(): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type RenderFunction = (props: ModalProps) => React.ReactNode;
|
||||||
|
|
||||||
|
export const Modals = mapMangledModuleLazy(".onAnimationEnd,", {
|
||||||
|
ModalRoot: filters.byCode("headerIdIsManaged:"),
|
||||||
|
ModalHeader: filters.byCode("children", "separator", "wrap", "NO_WRAP", "grow", "shrink", "id", "header"),
|
||||||
|
ModalContent: filters.byCode("scrollerRef", "content", "className", "children"),
|
||||||
|
ModalFooter: filters.byCode("HORIZONTAL_REVERSE", "START", "STRETCH", "NO_WRAP", "footerSeparator"),
|
||||||
|
ModalCloseButton: filters.byCode("closeWithCircleBackground", "hideOnFullscreen"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ModalRoot = (props: ModalRootProps) => <Modals.ModalRoot {...props} />;
|
||||||
|
export const ModalHeader = (props: any) => <Modals.ModalHeader {...props} />;
|
||||||
|
export const ModalContent = (props: any) => <Modals.ModalContent {...props} />;
|
||||||
|
export const ModalFooter = (props: any) => <Modals.ModalFooter {...props} />;
|
||||||
|
export const ModalCloseButton = (props: any) => <Modals.ModalCloseButton {...props} />;
|
||||||
|
|
||||||
|
const ModalAPI = mapMangledModuleLazy("onCloseRequest:null!=", {
|
||||||
|
openModal: filters.byCode("onCloseRequest:null!="),
|
||||||
|
closeModal: filters.byCode("onCloseCallback&&"),
|
||||||
|
openModalLazy: m => m?.length === 1 && filters.byCode(".apply(this,arguments)")(m),
|
||||||
|
});
|
||||||
|
|
||||||
|
export function openModalLazy(render: () => Promise<RenderFunction>, options?: ModalOptions & { contextKey?: string; }): Promise<string> {
|
||||||
|
return ModalAPI.openModalLazy(render, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openModal(render: RenderFunction, options?: ModalOptions, contextKey?: string): string {
|
||||||
|
return ModalAPI.openModal(render, options, contextKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeModal(modalKey: string, contextKey?: string): void {
|
||||||
|
return ModalAPI.closeModal(modalKey, contextKey);
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ export interface Patch {
|
||||||
find: string;
|
find: string;
|
||||||
replacement: PatchReplacement | PatchReplacement[];
|
replacement: PatchReplacement | PatchReplacement[];
|
||||||
all?: boolean;
|
all?: boolean;
|
||||||
|
predicate?(): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PluginAuthor {
|
export interface PluginAuthor {
|
||||||
|
@ -34,13 +35,101 @@ interface PluginDef {
|
||||||
start?(): void;
|
start?(): void;
|
||||||
stop?(): void;
|
stop?(): void;
|
||||||
patches?: Omit<Patch, "plugin">[];
|
patches?: Omit<Patch, "plugin">[];
|
||||||
|
/**
|
||||||
|
* List of commands. If you specify these, you must add CommandsAPI to dependencies
|
||||||
|
*/
|
||||||
commands?: Command[];
|
commands?: Command[];
|
||||||
|
/**
|
||||||
|
* A list of other plugins that your plugin depends on.
|
||||||
|
* These will automatically be enabled and loaded before your plugin
|
||||||
|
* Common examples are CommandsAPI, MessageEventsAPI...
|
||||||
|
*/
|
||||||
dependencies?: string[],
|
dependencies?: string[],
|
||||||
|
/**
|
||||||
|
* Whether this plugin is required and forcefully enabled
|
||||||
|
*/
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
/**
|
/**
|
||||||
* Set this if your plugin only works on Browser or Desktop, not both
|
* Set this if your plugin only works on Browser or Desktop, not both
|
||||||
*/
|
*/
|
||||||
target?: "WEB" | "DESKTOP" | "BOTH";
|
target?: "WEB" | "DESKTOP" | "BOTH";
|
||||||
|
/**
|
||||||
|
* Optionally provide settings that the user can configure in the Plugins tab of settings.
|
||||||
|
*/
|
||||||
|
options?: Record<string, PluginOptionsItem>;
|
||||||
|
/**
|
||||||
|
* Allows you to specify a custom Component that will be rendered in your
|
||||||
|
* plugin's settings page
|
||||||
|
*/
|
||||||
|
settingsAboutComponent?: React.ComponentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum OptionType {
|
||||||
|
STRING,
|
||||||
|
NUMBER,
|
||||||
|
BIGINT,
|
||||||
|
BOOLEAN,
|
||||||
|
SELECT,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PluginOptionsItem =
|
||||||
|
| PluginOptionString
|
||||||
|
| PluginOptionNumber
|
||||||
|
| PluginOptionBoolean
|
||||||
|
| PluginOptionSelect;
|
||||||
|
|
||||||
|
export interface PluginOptionBase {
|
||||||
|
description: string;
|
||||||
|
placeholder?: string;
|
||||||
|
onChange?(newValue: any): void;
|
||||||
|
disabled?(): boolean;
|
||||||
|
restartNeeded?: boolean;
|
||||||
|
componentProps?: Record<string, any>;
|
||||||
|
/**
|
||||||
|
* Set this if the setting only works on Browser or Desktop, not both
|
||||||
|
*/
|
||||||
|
target?: "WEB" | "DESKTOP" | "BOTH";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PluginOptionString extends PluginOptionBase {
|
||||||
|
type: OptionType.STRING;
|
||||||
|
/**
|
||||||
|
* Prevents the user from saving settings if this is false or a string
|
||||||
|
*/
|
||||||
|
isValid?(value: string): boolean | string;
|
||||||
|
default?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PluginOptionNumber extends PluginOptionBase {
|
||||||
|
type: OptionType.NUMBER | OptionType.BIGINT;
|
||||||
|
/**
|
||||||
|
* Prevents the user from saving settings if this is false or a string
|
||||||
|
*/
|
||||||
|
isValid?(value: number | BigInt): boolean | string;
|
||||||
|
default?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PluginOptionBoolean extends PluginOptionBase {
|
||||||
|
type: OptionType.BOOLEAN;
|
||||||
|
/**
|
||||||
|
* Prevents the user from saving settings if this is false or a string
|
||||||
|
*/
|
||||||
|
isValid?(value: boolean): boolean | string;
|
||||||
|
default?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PluginOptionSelect extends PluginOptionBase {
|
||||||
|
type: OptionType.SELECT;
|
||||||
|
/**
|
||||||
|
* Prevents the user from saving settings if this is false or a string
|
||||||
|
*/
|
||||||
|
isValid?(value: PluginOptionSelectOption): boolean | string;
|
||||||
|
options: PluginOptionSelectOption[];
|
||||||
|
}
|
||||||
|
export interface PluginOptionSelectOption {
|
||||||
|
label: string;
|
||||||
|
value: string | number | boolean;
|
||||||
|
default?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IpcRes<V = any> = { ok: true; value: V; } | { ok: false, error: any; };
|
export type IpcRes<V = any> = { ok: true; value: V; } | { ok: false, error: any; };
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import { waitFor, filters, _resolveReady } from "./webpack";
|
import { User } from "discord-types/general";
|
||||||
|
|
||||||
|
import { lazyWebpack } from "../utils/misc";
|
||||||
|
import { _resolveReady, filters, waitFor } from "./webpack";
|
||||||
|
|
||||||
import type Components from "discord-types/components";
|
import type Components from "discord-types/components";
|
||||||
import type Stores from "discord-types/stores";
|
import type Stores from "discord-types/stores";
|
||||||
import type Other from "discord-types/other";
|
import type Other from "discord-types/other";
|
||||||
import { lazyWebpack } from "../utils/misc";
|
|
||||||
|
|
||||||
export const Margins = lazyWebpack(filters.byProps(["marginTop20"]));
|
export const Margins = lazyWebpack(filters.byProps(["marginTop20"]));
|
||||||
|
|
||||||
export let FluxDispatcher: Other.FluxDispatcher;
|
export let FluxDispatcher: Other.FluxDispatcher;
|
||||||
|
@ -25,6 +27,10 @@ export let Button: any;
|
||||||
export let Switch: any;
|
export let Switch: any;
|
||||||
export let Tooltip: Components.Tooltip;
|
export let Tooltip: Components.Tooltip;
|
||||||
export let Router: any;
|
export let Router: any;
|
||||||
|
export let TextInput: any;
|
||||||
|
export let Text: (props: TextProps) => JSX.Element;
|
||||||
|
|
||||||
|
export const Select = lazyWebpack(filters.byCode("optionClassName", "popoutPosition", "autoFocus", "maxVisibleItems"));
|
||||||
|
|
||||||
export let Parser: any;
|
export let Parser: any;
|
||||||
export let Alerts: {
|
export let Alerts: {
|
||||||
|
@ -82,6 +88,10 @@ export const Toasts = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const UserUtils = {
|
||||||
|
fetchUser: lazyWebpack(filters.byCode(".USER(", "getUser")) as (id: string) => Promise<User>,
|
||||||
|
};
|
||||||
|
|
||||||
waitFor("useState", m => React = m);
|
waitFor("useState", m => React = m);
|
||||||
waitFor(["dispatch", "subscribe"], m => {
|
waitFor(["dispatch", "subscribe"], m => {
|
||||||
FluxDispatcher = m;
|
FluxDispatcher = m;
|
||||||
|
@ -120,3 +130,23 @@ waitFor(["show", "close"], m => Alerts = m);
|
||||||
waitFor("parseTopic", m => Parser = m);
|
waitFor("parseTopic", m => Parser = m);
|
||||||
|
|
||||||
waitFor(["open", "saveAccountChanges"], m => Router = m);
|
waitFor(["open", "saveAccountChanges"], m => Router = m);
|
||||||
|
waitFor(["defaultProps", "Sizes", "contextType"], m => TextInput = m);
|
||||||
|
|
||||||
|
waitFor(m => {
|
||||||
|
if (typeof m !== "function") return false;
|
||||||
|
const s = m.toString();
|
||||||
|
return (s.length < 1500 && s.includes("data-text-variant") && s.includes("always-white"));
|
||||||
|
}, m => Text = m);
|
||||||
|
|
||||||
|
export type TextProps = React.PropsWithChildren & {
|
||||||
|
variant: TextVariant;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
color?: string;
|
||||||
|
tag?: "div" | "span" | "p" | "strong";
|
||||||
|
selectable?: boolean;
|
||||||
|
lineClamp?: number;
|
||||||
|
id?: string;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TextVariant = "heading-sm/normal" | "heading-sm/medium" | "heading-sm/bold" | "heading-md/normal" | "heading-md/medium" | "heading-md/bold" | "heading-lg/normal" | "heading-lg/medium" | "heading-lg/bold" | "heading-xl/normal" | "heading-xl/medium" | "heading-xl/bold" | "heading-xxl/normal" | "heading-xxl/medium" | "heading-xxl/bold" | "eyebrow" | "heading-deprecated-14/normal" | "heading-deprecated-14/medium" | "heading-deprecated-14/bold" | "text-xxs/normal" | "text-xxs/medium" | "text-xxs/semibold" | "text-xxs/bold" | "text-xs/normal" | "text-xs/medium" | "text-xs/semibold" | "text-xs/bold" | "text-sm/normal" | "text-sm/medium" | "text-sm/semibold" | "text-sm/bold" | "text-md/normal" | "text-md/medium" | "text-md/semibold" | "text-md/bold" | "text-lg/normal" | "text-lg/medium" | "text-lg/semibold" | "text-lg/bold" | "display-md" | "display-lg" | "code";
|
||||||
|
|
|
@ -100,6 +100,7 @@ function patchPush() {
|
||||||
|
|
||||||
for (let i = 0; i < patches.length; i++) {
|
for (let i = 0; i < patches.length; i++) {
|
||||||
const patch = patches[i];
|
const patch = patches[i];
|
||||||
|
if (patch.predicate && !patch.predicate()) continue;
|
||||||
if (code.includes(patch.find)) {
|
if (code.includes(patch.find)) {
|
||||||
patchedBy.add(patch.plugin);
|
patchedBy.add(patch.plugin);
|
||||||
// @ts-ignore we change all patch.replacement to array in plugins/index
|
// @ts-ignore we change all patch.replacement to array in plugins/index
|
||||||
|
|
Loading…
Reference in a new issue