diff --git a/UserpluginInstallButton.tsx b/UserpluginInstallButton.tsx
index 0bb50c4..4246824 100644
--- a/UserpluginInstallButton.tsx
+++ b/UserpluginInstallButton.tsx
@@ -1,6 +1,6 @@
-import { Button, ChannelStore } from "@webpack/common";
+import { Alerts, Button, ChannelStore, Toasts } from "@webpack/common";
import { Message } from "discord-types/general";
-import { CLONE_LINK_REGEX, clonePlugin } from ".";
+import { CLONE_LINK_REGEX, clonePlugin, Native, plugins } from ".";
const WHITELISTED_SHARE_CHANNELS = ["1256395889354997771", "1032200195582197831", "1301947896601509900"];
@@ -10,10 +10,53 @@ export default function UserpluginInstallButton({ props }: any) {
return;
const gitLink = (props.message.content as string).match(CLONE_LINK_REGEX);
if (!gitLink) return;
- return clonePlugin(gitLink)}>
- Install plugin
- ;
+ const installed = plugins.includes(gitLink[1]);
+ return <>
+
+ clonePlugin(gitLink)}>
+ {installed ? "Reinstall" : "Install"} plugin
+
+ {
+ installed && {
+ Alerts.show({
+ title: "Uninstall plugin",
+ body: `Are you sure that you want to uninstall ${gitLink[1]}?`,
+ cancelText: "Cancel",
+ confirmColor: Button.Colors.RED,
+ confirmText: "Uninstall",
+ async onConfirm() {
+ Toasts.show({
+ id: Toasts.genId(),
+ message: `Uninstalling ${gitLink[1]}...`,
+ type: Toasts.Type.MESSAGE
+ });
+ try {
+ await Native.deleteFolder(`${VesktopNative.fileManager.getVencordDir().replace("\\", "/")}/../src/userplugins/${gitLink[1]}`);
+ await Native.build(VesktopNative.fileManager.getVencordDir().replace("\\", "/"));
+ window.location.reload();
+ }
+ catch {
+ Toasts.pop();
+ return Toasts.show({
+ message: "Something bad has happened while deleting the plugin.",
+ id: Toasts.genId(),
+ type: Toasts.Type.FAILURE
+ });
+ }
+ },
+ });
+ }}>
+ Uninstall plugin
+
+ }
+
+ >;
}
diff --git a/UserpluginInstallModal.tsx b/UserpluginInstallModal.tsx
new file mode 100644
index 0000000..0427870
--- /dev/null
+++ b/UserpluginInstallModal.tsx
@@ -0,0 +1,80 @@
+import { classNameFactory } from "@api/Styles";
+import ErrorBoundary from "@components/ErrorBoundary";
+import { PluginCard } from "@components/PluginSettings";
+import { ModalCloseButton, ModalContent, ModalHeader, ModalRoot, ModalSize, openModal, closeModal } from "@utils/modal";
+import { Alerts, Button, Card, Parser, Text, Toasts } from "@webpack/common";
+import { Native } from ".";
+
+const cl = classNameFactory("vc-userplugininstaller-");
+
+export function showInstallModal(meta: any, pluginPath: string) {
+ console.log(meta);
+ Alerts.show({
+ title: <>
+
+ Review userplugin
+
+ >,
+ body: <>
+
+
+ Plugin info
+
+
+
+ {meta.name}
+ {meta.description}
+
+
+
+ Notes & warnings
+
+ {
+ meta.usesPreSend &&
+
+ Has pre-send listeners
+ This plugin is able to intercept and edit messages before sending them.
+
+
+ }
+
+
+ Refresh and enable required
+ After the install is completed, Discord will be reloaded and you will need to enable the plugin yourself.
+
+
+ Make sure that the plugin author is trustworthy before installing {meta.name}.
+
+ >,
+ cancelText: "Cancel install",
+ confirmText: `Install plugin`,
+ onCancel() {
+ Native.deleteFolder(pluginPath);
+ },
+ onCloseCallback() {
+ setTimeout(() => {
+ Native.deleteFolder(pluginPath);
+ }, 20000);
+ },
+ async onConfirm() {
+ Toasts.pop();
+ Toasts.show({
+ message: "Rebuilding Vencord, please wait...",
+ id: Toasts.genId(),
+ type: Toasts.Type.MESSAGE
+ });
+ try {
+ await Native.build(pluginPath);
+ window.location.reload();
+ }
+ catch {
+ Toasts.pop();
+ return Toasts.show({
+ message: "Something bad has happened while building Vencord.",
+ id: Toasts.genId(),
+ type: Toasts.Type.FAILURE
+ });
+ }
+ },
+ });
+}
diff --git a/index.tsx b/index.tsx
index 701d409..e5db3f9 100644
--- a/index.tsx
+++ b/index.tsx
@@ -6,11 +6,15 @@ import { Button, ChannelStore, Forms, Toasts } from "@webpack/common";
import { Message } from "discord-types/general";
import { clone } from "lodash";
import UserpluginInstallButton from "./UserpluginInstallButton";
+import { showInstallModal } from "./UserpluginInstallModal";
+import "./style.css";
+
+export let plugins: any[] = [];
export const CLONE_LINK_REGEX = /https:\/\/(?:git(?:hub|lab)\.com|git\.(?:[a-zA-Z0-9]|\.)+|codeberg\.org)\/(?!user-attachments)(?:[a-zA-Z0-9]|-)+\/((?:[a-zA-Z0-9]|-)+)(?:\.git)?(?:\/)?/;
// @ts-ignore
-const Native = VencordNative.pluginHelpers.UserpluginInstaller as PluginNative;
+export const Native = VencordNative.pluginHelpers.UserpluginInstaller as PluginNative;
export async function clonePlugin(gitLink: RegExpMatchArray) {
Toasts.show({
@@ -19,7 +23,11 @@ export async function clonePlugin(gitLink: RegExpMatchArray) {
type: Toasts.Type.MESSAGE
});
try {
- const clonedRepo = await Native.cloneRepo(gitLink[0], `${VesktopNative.fileManager.getVencordDir().replace("\\", "/")}/../src/userplugins/${gitLink[1]}`);
+ const path = `${VesktopNative.fileManager.getVencordDir().replace("\\", "/")}/../src/userplugins/${gitLink[1]}`;
+ const clonedRepo = await Native.cloneRepo(gitLink[0], path);
+ const meta = await Native.getPluginMeta(path);
+ console.log(meta);
+ showInstallModal(meta, path);
}
catch (e) {
Toasts.pop();
@@ -35,7 +43,9 @@ export default definePlugin({
name: "UserpluginInstaller",
description: "Install userplugins with a simple button click",
authors: [Devs.nin0dev],
- start() {
+ async start() {
+ plugins = await Native.getPlugins(`${VesktopNative.fileManager.getVencordDir().replace("\\", "/")}/../src/userplugins/`);
+ console.log(plugins);
addAccessory("userpluginInstallButton", (props: Record) => (
), 4);
diff --git a/native.ts b/native.ts
index af6de52..3fe3839 100644
--- a/native.ts
+++ b/native.ts
@@ -1,7 +1,11 @@
import { exec, spawn } from "child_process";
import { IpcMainInvokeEvent } from "electron";
+import { read, readdir, readdirSync, readFileSync, rmSync } from "fs";
+
+const PLUGIN_META_REGEX = /export default definePlugin\((?:\s|\/(?:\/|\*).*)*{\s*(?:\s|\/(?:\/|\*).*)*name:\s*(?:"|'|`)(.*)(?:"|'|`)(?:\s|\/(?:\/|\*).*)*,(?:\s|\/(?:\/|\*).*)*(?:\s|\/(?:\/|\*).*)*description:\s*(?:"|'|`)(.*)(?:"|'|`)(?:\s|\/(?:\/|\*).*)*/;
export async function cloneRepo(_: IpcMainInvokeEvent, repo: string, clonePath: string): Promise {
+ rmSync(clonePath, { recursive: true, force: true });
return new Promise((resolve, reject) => {
exec(`git clone ${repo} ${clonePath}`, (error, stdout, stderr) => {
if (error) {
@@ -13,3 +17,50 @@ export async function cloneRepo(_: IpcMainInvokeEvent, repo: string, clonePath:
});
});
}
+
+export async function getPluginMeta(_, path: string): Promise {
+ return new Promise((resolve, reject) => {
+ const files = readdirSync(path);
+ let fileToRead: "index.ts" | "index.tsx" | "index.js" | "index.jsx" | undefined;
+ files.forEach(f => {
+ if (f === "index.ts") fileToRead = "index.ts";
+ if (f === "index.tsx") fileToRead = "index.tsx";
+ if (f === "index.js") fileToRead = "index.js";
+ if (f === "index.jsx") fileToRead = "index.jsx";
+ });
+ if (!fileToRead) reject();
+
+ const file = readFileSync(`${path}/${fileToRead}`, "utf8");
+ const rawMeta = file.match(PLUGIN_META_REGEX);
+ resolve({
+ name: rawMeta![1],
+ description: rawMeta![2],
+ usesPreSend: file.includes("PreSendListener")
+ });
+ });
+}
+
+export function deleteFolder(_, path: string) {
+ rmSync(path, { recursive: true, force: true });
+}
+
+export async function build(_: IpcMainInvokeEvent, path: string): Promise {
+ return new Promise((resolve, reject) => {
+ exec(`pnpm build`, {
+ cwd: path
+ }, (error, stdout, stderr) => {
+ if (error) {
+ return reject(
+ stderr
+ );
+ }
+ resolve(null);
+ });
+ });
+}
+
+export async function getPlugins(_, path: string): Promise {
+ return new Promise((resolve) => {
+ return resolve(readdirSync(path));
+ });
+}
diff --git a/style.css b/style.css
new file mode 100644
index 0000000..065156e
--- /dev/null
+++ b/style.css
@@ -0,0 +1,51 @@
+.vc-userplugininstaller-subheader {
+ font-weight: 500;
+ font-size: 1.2rem;
+}
+
+.vc-userplugininstaller-body {
+ text-align: left;
+}
+
+.vc-userplugininstaller-title {
+ text-align: left;
+ font-size: 1.25rem;
+ font-weight: 600;
+ text-transform: none;
+}
+
+.vc-userplugininstaller-close {
+ margin-left: auto;
+}
+
+.vc-userplugininstaller-card {
+ padding: 10px;
+ margin: 10px 0;
+}
+
+.vc-userplugininstaller-lead-card {
+ background-color: var(--bg-mod-strong) !important;
+ border: 1px solid var(--bg-brand);
+}
+
+.vc-userplugininstaller-name {
+ font-size: 1.15rem;
+ font-weight: 500;
+ margin-bottom: 10px;
+}
+
+.vc-userplugininstaller-warning-card {
+ background: var(--info-warning-background);
+ border: 1px solid var(--info-warning-foreground);
+ color: var(--info-warning-text);
+}
+
+.vc-userplugininstaller-row {
+ display: flex;
+ margin-top: 12px;
+}
+
+.vc-userplugininstaller-row * {
+ flex: 1;
+ margin: 0 10px;
+}