Equicord/src/components/settings/tabs/plugins/PluginModal.tsx

393 lines
16 KiB
TypeScript
Raw Normal View History

2022-10-22 01:17:06 +02:00
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "./PluginModal.css";
import { generateId } from "@api/Commands";
import { Settings, useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
2025-10-11 21:50:14 -04:00
import { BaseText } from "@components/BaseText";
import { Button } from "@components/Button";
2022-11-28 13:58:14 +01:00
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
2025-10-11 21:50:14 -04:00
import { Paragraph } from "@components/Paragraph";
import { debounce } from "@shared/debounce";
import { gitRemote } from "@shared/vencordUserAgent";
2023-05-06 01:36:00 +02:00
import { proxyLazy } from "@utils/lazy";
import { Margins } from "@utils/margins";
2025-07-21 12:32:14 +02:00
import { classes, isObjectEmpty } from "@utils/misc";
2024-06-29 20:18:29 +02:00
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
2025-11-26 15:13:06 -05:00
import { t } from "@utils/translation";
import { OptionType, Plugin } from "@utils/types";
import { User } from "@vencord/discord-types";
import { findByPropsLazy } from "@webpack";
import { Clickable, FluxDispatcher, React, Toasts, Tooltip, useEffect, UserStore, UserSummaryItem, UserUtils, useState } from "@webpack/common";
import { Constructor } from "type-fest";
import { PluginMeta } from "~plugins";
import { OptionComponentMap } from "./components";
import { openContributorModal } from "./ContributorModal";
2025-09-25 18:58:46 -04:00
import { GithubButton, WebsiteButton } from "./LinkIconButton";
const cl = classNameFactory("vc-plugin-modal-");
const AvatarStyles = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar");
const UserRecord: Constructor<Partial<User>> = proxyLazy(() => UserStore.getCurrentUser().constructor) as any;
interface PluginModalProps extends ModalProps {
plugin: Plugin;
onRestartNeeded(key: string): void;
}
2025-06-18 11:53:57 -04:00
export function makeDummyUser(user: { username: string; id?: string; avatar?: string; }) {
const newUser = new UserRecord({
2023-05-23 01:55:39 +02:00
username: user.username,
id: user.id ?? generateId(),
avatar: user.avatar,
/** To stop discord making unwanted requests... */
bot: true,
});
FluxDispatcher.dispatch({
type: "USER_UPDATE",
user: newUser,
});
return newUser;
}
export default function PluginModal({ plugin, onRestartNeeded, onClose, transitionState }: PluginModalProps) {
const pluginSettings = useSettings([`plugins.${plugin.name}.*`]).plugins[plugin.name];
const hasSettings = Boolean(pluginSettings && plugin.options && !isObjectEmpty(plugin.options));
const [authors, setAuthors] = useState<Partial<User>[]>([]);
useEffect(() => {
(async () => {
for (const user of plugin.authors.slice(0, 6)) {
try {
const author = user.id
? await UserUtils.getUser(String(user.id))
.catch(() => makeDummyUser({ username: user.name }))
: makeDummyUser({ username: user.name });
setAuthors(a => [...a, author]);
} catch (e) {
continue;
}
}
})();
}, [plugin.authors]);
function handleResetClick() {
openWarningModal(plugin, { onClose, transitionState }, onRestartNeeded);
}
function renderSettings() {
if (!hasSettings || !plugin.options)
2025-10-11 21:50:14 -04:00
return <Paragraph>There are no settings for this plugin.</Paragraph>;
const options = Object.entries(plugin.options).map(([key, setting]) => {
if (setting.type === OptionType.CUSTOM || setting.hidden) return null;
function onChange(newValue: any) {
const option = plugin.options?.[key];
if (!option || option.type === OptionType.CUSTOM) return;
pluginSettings[key] = newValue;
if (option.restartNeeded) onRestartNeeded(key);
}
const Component = OptionComponentMap[setting.type];
return (
2025-10-09 21:01:48 +02:00
<ErrorBoundary noop key={key}>
<Component
id={key}
option={setting}
onChange={debounce(onChange)}
pluginSettings={pluginSettings}
definedSettings={plugin.settings}
/>
</ErrorBoundary>
);
});
return (
<div className="vc-plugins-settings">
{options}
</div>
);
}
2025-05-04 00:21:12 -04:00
function renderMoreUsers(_label: string) {
const remainingAuthors = plugin.authors.slice(6);
return (
2025-05-04 00:21:12 -04:00
<Tooltip text={remainingAuthors.map(u => u.name).join(", ")}>
{({ onMouseEnter, onMouseLeave }) => (
<div
className={AvatarStyles.moreUsers}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
2025-05-04 00:21:12 -04:00
+{remainingAuthors.length}
</div>
)}
</Tooltip>
);
}
const pluginMeta = PluginMeta[plugin.name];
2025-09-25 18:58:46 -04:00
const isEquicordPlugin = pluginMeta.folderName.startsWith("src/equicordplugins/") ?? false;
return (
<ModalRoot transitionState={transitionState} size={ModalSize.MEDIUM}>
2025-07-21 12:32:14 +02:00
<ModalHeader separator={false} className={Margins.bottom8}>
2025-10-11 21:50:14 -04:00
<BaseText size="xl" weight="bold" style={{ flexGrow: 1 }}>{plugin.name}</BaseText>
<ModalCloseButton onClick={onClose} />
</ModalHeader>
2025-07-21 12:32:14 +02:00
<ModalContent className={Margins.bottom16}>
2025-10-09 21:01:48 +02:00
<section>
<Flex className={cl("info")}>
2025-10-11 21:50:14 -04:00
<Paragraph className={cl("description")}>{plugin.description}</Paragraph>
{!pluginMeta.userPlugin && (
<div className="vc-settings-modal-links">
2025-09-25 18:58:46 -04:00
<WebsiteButton
text="View more info"
href={isEquicordPlugin ? `https://equicord.org/plugins/${plugin.name}` : `https://vencord.dev/plugins/${plugin.name}`}
/>
<GithubButton
text="View source code"
2024-08-25 01:11:16 -04:00
href={`https://github.com/${gitRemote}/tree/main/${pluginMeta.folderName}`}
/>
</div>
)}
</Flex>
2025-10-11 21:50:14 -04:00
<BaseText size="lg" weight="semibold" className={classes(Margins.top8, Margins.bottom8)}>Authors</BaseText>
2025-07-21 12:32:14 +02:00
<div style={{ width: "fit-content" }}>
<ErrorBoundary noop>
<UserSummaryItem
users={authors}
guildId={undefined}
renderIcon={false}
showDefaultAvatarsForNullUsers
renderMoreUsers={renderMoreUsers}
renderUser={(user: User) => (
<Clickable
className={AvatarStyles.clickableAvatar}
2025-11-11 11:22:01 -05:00
onClick={() => isEquicordPlugin ? openContributorModal(user) : openContributorModal(user)}
>
<img
className={AvatarStyles.avatar}
src={user.getAvatarURL(void 0, 80, true)}
alt={user.username}
title={user.username}
/>
</Clickable>
)}
/>
</ErrorBoundary>
</div>
2025-10-09 21:01:48 +02:00
</section>
{!!plugin.settingsAboutComponent && (
2025-07-21 12:32:14 +02:00
<div className={Margins.top16}>
2025-10-09 21:01:48 +02:00
<section>
<ErrorBoundary message="An error occurred while rendering this plugin's custom Info Component">
<plugin.settingsAboutComponent />
</ErrorBoundary>
2025-10-09 21:01:48 +02:00
</section>
</div>
)}
2025-10-09 21:01:48 +02:00
<section>
2025-10-11 21:50:14 -04:00
<BaseText size="lg" weight="semibold" className={classes(Margins.top16, Margins.bottom8)}>Settings</BaseText>
{renderSettings()}
2025-10-09 21:01:48 +02:00
</section>
2025-07-15 14:56:05 -04:00
</ModalContent>
{
hasSettings && <ModalFooter>
<Flex flexDirection="column" style={{ width: "100%" }}>
<Flex style={{ justifyContent: "space-between" }}>
<Tooltip text="Reset to default settings" shouldShow={!isObjectEmpty(pluginSettings)}>
{({ onMouseEnter, onMouseLeave }) => (
<Button
2025-09-13 00:53:03 -04:00
className={cl("disable-warning")}
size="small"
variant="primary"
onClick={handleResetClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
Reset
</Button>
)}
</Tooltip>
</Flex>
</Flex>
</ModalFooter>
}
</ModalRoot >
);
}
2024-06-29 20:18:29 +02:00
export function openPluginModal(plugin: Plugin, onRestartNeeded?: (pluginName: string, key: string) => void) {
2024-06-29 20:18:29 +02:00
openModal(modalProps => (
<PluginModal
{...modalProps}
plugin={plugin}
onRestartNeeded={(key: string) => onRestartNeeded?.(plugin.name, key)}
2024-06-29 20:18:29 +02:00
/>
));
}
function resetSettings(plugin: Plugin, warningModalProps?: ModalProps, pluginModalProps?: ModalProps, onRestartNeeded?: (pluginName: string) => void) {
const defaultSettings = plugin.settings?.def;
const pluginName = plugin.name;
if (!defaultSettings) {
console.error(`No default settings found for ${pluginName}`);
return;
}
const newSettings: Record<string, any> = {};
let restartNeeded = false;
for (const key in defaultSettings) {
if (key === "enabled") continue;
const setting = defaultSettings[key];
setting.type = setting.type ?? OptionType.STRING;
if (setting.type === OptionType.STRING) {
newSettings[key] = setting.default !== undefined && setting.default !== "" ? setting.default : "";
} else if ("default" in setting && setting.default !== undefined) {
newSettings[key] = setting.default;
}
if (setting?.restartNeeded) {
restartNeeded = true;
}
}
const currentSettings = plugin.settings?.store;
if (currentSettings) {
Object.assign(currentSettings, newSettings);
}
if (restartNeeded) {
onRestartNeeded?.(plugin.name);
}
Toasts.show({
message: `Settings for ${pluginName} have been reset.`,
id: Toasts.genId(),
type: Toasts.Type.SUCCESS,
options: {
position: Toasts.Position.TOP
}
});
warningModalProps?.onClose();
pluginModalProps?.onClose();
}
export function openWarningModal(plugin?: Plugin | null, pluginModalProps?: ModalProps | null, onRestartNeeded?: (pluginName: string) => void, isPlugin = true, enabledPlugins?: number | null, reset?: any) {
if (Settings.ignoreResetWarning && isPlugin) {
if (plugin && pluginModalProps) return resetSettings(plugin, pluginModalProps, pluginModalProps, onRestartNeeded);
return;
} else if (Settings.ignoreResetWarning && !isPlugin) {
return reset();
}
const text = isPlugin
? `You are about to reset all settings for ${plugin?.name} to their default values.`
: `You are about to disable ${enabledPlugins} plugins!`;
2024-10-19 01:19:15 -04:00
openModal(warningModalProps => (
<ModalRoot
{...warningModalProps}
size={ModalSize.SMALL}
className="vc-text-selectable"
transitionState={warningModalProps.transitionState}
>
<ModalHeader separator={false}>
2025-10-11 21:50:14 -04:00
<BaseText className="text-danger">Dangerous Action</BaseText>
<ModalCloseButton onClick={warningModalProps.onClose} className="vc-modal-close-button" />
</ModalHeader>
<ModalContent>
2025-10-09 18:48:30 -04:00
<section>
<Flex className="vc-warning-info">
2025-10-11 21:50:14 -04:00
<BaseText className="text-normal">
{text}
2025-10-11 21:50:14 -04:00
</BaseText>
<BaseText className="warning-text">
THIS ACTION IS IRREVERSIBLE!
2025-10-11 21:50:14 -04:00
</BaseText>
<BaseText className="text-normal margin-bottom">
If you are certain you want to proceed, click <strong>Confirm Reset</strong>. Otherwise, click <strong>Cancel</strong>.
2025-10-11 21:50:14 -04:00
</BaseText>
</Flex>
2025-10-09 18:48:30 -04:00
</section>
</ModalContent>
<ModalFooter className="vc-modal-footer">
<Flex className="vc-button-container">
<Flex className="button-group">
2025-09-13 00:53:03 -04:00
<Button
size="small"
variant="secondary"
2025-09-13 00:53:03 -04:00
onClick={warningModalProps.onClose}
>
2025-11-26 15:13:06 -05:00
{t("vencord.cancel")}
2025-09-13 00:53:03 -04:00
</Button>
{!Settings.ignoreResetWarning && (
<Button
size="small"
2025-09-13 00:53:03 -04:00
className={cl("disable-warning")}
onClick={() => {
Settings.ignoreResetWarning = true;
}}
>
Disable Warning Forever
</Button>
)}
2025-09-13 00:53:03 -04:00
<Button
size="small"
2025-09-13 00:53:03 -04:00
onClick={() => {
if (isPlugin) {
if (plugin && pluginModalProps)
resetSettings(plugin, pluginModalProps, pluginModalProps, onRestartNeeded);
} else {
reset();
}
2025-09-13 00:53:03 -04:00
}}
className={cl("confirm-reset")}
>
Confirm Reset
</Button>
</Flex>
</Flex>
</ModalFooter>
</ModalRoot>
));
}