parent
ae730e8398
commit
5625d63e46
@ -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>
|
||||
);
|
||||
}
|
@ -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";
|
@ -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] || [];
|
||||
}
|
@ -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,2 +1,3 @@
|
||||
export { default as Settings } from "./Settings";
|
||||
export { default as PluginSettings } from "./PluginSettings";
|
||||
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;"
|
||||
}
|
||||
}]
|
||||
});
|
@ -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,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 ",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
Loading…
Reference in new issue