parent
							
								
									ae730e8398
								
							
						
					
					
						commit
						5625d63e46
					
				
					 22 changed files with 945 additions and 192 deletions
				
			
		|  | @ -5,6 +5,7 @@ import { ErrorCard } from "./ErrorCard"; | |||
| interface Props { | ||||
|     fallback?: React.ComponentType<React.PropsWithChildren<{ error: any; message: string; stack: string; }>>; | ||||
|     onError?(error: Error, errorInfo: React.ErrorInfo): void; | ||||
|     message?: string; | ||||
| } | ||||
| 
 | ||||
| const color = "#e78284"; | ||||
|  | @ -58,15 +59,14 @@ export default class ErrorBoundary extends React.Component<React.PropsWithChildr | |||
|                 {...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 ( | ||||
|             <ErrorCard style={{ | ||||
|                 overflow: "hidden", | ||||
|             }}> | ||||
|                 <h1>Oh no!</h1> | ||||
|                 <p> | ||||
|                     An error occurred while rendering this Component. More info can be found below | ||||
|                     and in your console. | ||||
|                 </p> | ||||
|                 <p>{msg}</p> | ||||
|                 <code> | ||||
|                     {this.state.message} | ||||
|                     {!!this.state.stack && ( | ||||
|  |  | |||
|  | @ -4,7 +4,7 @@ export function Flex(props: React.PropsWithChildren<{ | |||
|     flexDirection?: React.CSSProperties["flexDirection"]; | ||||
|     style?: React.CSSProperties; | ||||
|     className?: string; | ||||
| }>) { | ||||
| } & React.HTMLProps<HTMLDivElement>>) { | ||||
|     props.style ??= {}; | ||||
|     props.style.flexDirection ||= props.flexDirection; | ||||
|     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 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"; | ||||
| 
 | ||||
| function showErrorToast(message: string) { | ||||
|     Toasts.show({ | ||||
|         message, | ||||
|         type: Toasts.Type.FAILURE, | ||||
|         id: Toasts.genId(), | ||||
|         options: { | ||||
|             position: Toasts.Position.BOTTOM | ||||
|         } | ||||
|     }); | ||||
| } | ||||
| import IpcEvents from "../utils/IpcEvents"; | ||||
| import { useAwaiter } from "../utils/misc"; | ||||
| import { Alerts, Button, Forms, Margins, Parser, React, Switch } from "../webpack/common"; | ||||
| import ErrorBoundary from "./ErrorBoundary"; | ||||
| import { Flex } from "./Flex"; | ||||
| 
 | ||||
| export default ErrorBoundary.wrap(function Settings() { | ||||
|     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 ( | ||||
|         <Forms.FormSection tag="h1" title="Vencord"> | ||||
|  | @ -69,10 +39,10 @@ export default ErrorBoundary.wrap(function Settings() { | |||
|             </Forms.FormTitle> | ||||
| 
 | ||||
|             <Forms.FormText> | ||||
|                 SettingsDir: <code style={{ userSelect: "text", cursor: "text" }}>{settingsDir}</code> | ||||
|                 Settings Directory: <code style={{ userSelect: "text", cursor: "text" }}>{settingsDir}</code> | ||||
|             </Forms.FormText> | ||||
| 
 | ||||
|             {!IS_WEB && <Flex className={Margins.marginBottom20}> | ||||
|             {!IS_WEB && <Flex className={Margins.marginBottom20} style={{ marginTop: 8 }}> | ||||
|                 <Button | ||||
|                     onClick={() => window.DiscordNative.app.relaunch()} | ||||
|                     size={Button.Sizes.SMALL} | ||||
|  | @ -118,58 +88,6 @@ export default ErrorBoundary.wrap(function Settings() { | |||
|             > | ||||
|                 Get notified about new Updates | ||||
|             </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 > | ||||
|     ); | ||||
| }); | ||||
|  |  | |||
|  | @ -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;" | ||||
|         } | ||||
|     }] | ||||
| }); | ||||
							
								
								
									
										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 { registerCommand, unregisterCommand } from "../api/Commands"; | ||||
| import { Settings } from "../api/settings"; | ||||
| 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 { Devs } from "../utils/constants"; | ||||
| import definePlugin from "../utils/types"; | ||||
| 
 | ||||
| export default definePlugin({ | ||||
|     name: "Settings", | ||||
|     description: "Adds Settings UI and debug info", | ||||
|     authors: [Devs.Ven], | ||||
|     authors: [Devs.Ven, Devs.Megu], | ||||
|     required: true, | ||||
|     patches: [{ | ||||
|         find: "().versionHash", | ||||
|  | @ -33,6 +34,7 @@ export default definePlugin({ | |||
|                 return ( | ||||
|                     `{section:${mod}.ID.HEADER,label:"Vencord"},` + | ||||
|                     '{section:"VencordSetting",label:"Vencord",element:Vencord.Components.Settings},' + | ||||
|                     '{section:"VencordPlugins",label:"Plugins",element:Vencord.Components.PluginSettings},' + | ||||
|                     updater + | ||||
|                     `{section:${mod}.ID.DIVIDER},${m}` | ||||
|                 ); | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import { Devs } from "../utils/constants"; | ||||
| import definePlugin from "../utils/types"; | ||||
| import { lazyWebpack, makeLazy } from "../utils/misc"; | ||||
| import { ModalSize, openModal } from "../utils/modal"; | ||||
| import { ModalRoot, ModalSize, openModal } from "../utils/modal"; | ||||
| import { find } from "../webpack"; | ||||
| 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.", | ||||
| 
 | ||||
|     openImage(url: string) { | ||||
|         openModal(() => ( | ||||
|             <ImageModal | ||||
|                 shouldAnimate={true} | ||||
|                 original={url} | ||||
|                 src={url} | ||||
|                 renderLinkComponent={props => React.createElement(getMaskedLink(), props)} | ||||
|             /> | ||||
|         ), { size: ModalSize.DYNAMIC }); | ||||
|         openModal(modalProps => ( | ||||
|             <ModalRoot size={ModalSize.DYNAMIC} {...modalProps}> | ||||
|                 <ImageModal | ||||
|                     shouldAnimate={true} | ||||
|                     original={url} | ||||
|                     src={url} | ||||
|                     renderLinkComponent={props => React.createElement(getMaskedLink(), props)} | ||||
|                 /> | ||||
|             </ModalRoot> | ||||
|         )); | ||||
|     }, | ||||
| 
 | ||||
|     patches: [ | ||||
|  |  | |||
|  | @ -1,15 +1,6 @@ | |||
| import { filters } from "../webpack"; | ||||
| import { lazyWebpack } from "./misc"; | ||||
| 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 { | ||||
|     SMALL = "small", | ||||
|     MEDIUM = "medium", | ||||
|  | @ -17,26 +8,64 @@ export enum ModalSize { | |||
|     DYNAMIC = "dynamic", | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Open a modal | ||||
|  * @param Component The component to render in the modal | ||||
|  * @returns The key of this modal. This can be used to close the modal later with closeModal | ||||
|  */ | ||||
| export function openModal(Component: React.ComponentType, modalProps: Record<string, any>) { | ||||
|     let key = `Vencord${modalId++}`; | ||||
|     Modals.openModal(props => ( | ||||
|         <ModalRoot {...props} {...modalProps}> | ||||
|             <Component /> | ||||
|         </ModalRoot> | ||||
|     ), { modalKey: key }); | ||||
| 
 | ||||
|     return key; | ||||
| enum ModalTransitionState { | ||||
|     ENTERING, | ||||
|     ENTERED, | ||||
|     EXITING, | ||||
|     EXITED, | ||||
|     HIDDEN, | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Close a modal by key. The id you need for this is returned by openModal. | ||||
|  * @param key The key of the modal to close | ||||
|  */ | ||||
| export function closeModal(key: string) { | ||||
|     Modals.closeModal(key); | ||||
| export interface ModalProps { | ||||
|     transitionState: ModalTransitionState; | ||||
|     onClose(): Promise<void>; | ||||
| } | ||||
| 
 | ||||
| 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; | ||||
|     replacement: PatchReplacement | PatchReplacement[]; | ||||
|     all?: boolean; | ||||
|     predicate?(): boolean; | ||||
| } | ||||
| 
 | ||||
| export interface PluginAuthor { | ||||
|  | @ -34,13 +35,101 @@ interface PluginDef { | |||
|     start?(): void; | ||||
|     stop?(): void; | ||||
|     patches?: Omit<Patch, "plugin">[]; | ||||
|     /** | ||||
|      * List of commands. If you specify these, you must add CommandsAPI to dependencies | ||||
|      */ | ||||
|     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[], | ||||
|     /** | ||||
|      * Whether this plugin is required and forcefully enabled | ||||
|      */ | ||||
|     required?: boolean; | ||||
|     /** | ||||
|      * Set this if your plugin only works on Browser or Desktop, not 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; }; | ||||
|  |  | |||
|  | @ -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 Stores from "discord-types/stores"; | ||||
| import type Other from "discord-types/other"; | ||||
| import { lazyWebpack } from "../utils/misc"; | ||||
| 
 | ||||
| export const Margins = lazyWebpack(filters.byProps(["marginTop20"])); | ||||
| 
 | ||||
| export let FluxDispatcher: Other.FluxDispatcher; | ||||
|  | @ -25,6 +27,10 @@ export let Button: any; | |||
| export let Switch: any; | ||||
| export let Tooltip: Components.Tooltip; | ||||
| 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 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(["dispatch", "subscribe"], m => { | ||||
|     FluxDispatcher = m; | ||||
|  | @ -120,3 +130,23 @@ waitFor(["show", "close"], m => Alerts = m); | |||
| waitFor("parseTopic", m => Parser = 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++) { | ||||
|                     const patch = patches[i]; | ||||
|                     if (patch.predicate && !patch.predicate()) continue; | ||||
|                     if (code.includes(patch.find)) { | ||||
|                         patchedBy.add(patch.plugin); | ||||
|                         // @ts-ignore we change all patch.replacement to array in plugins/index
 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue