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 ; + const installed = plugins.includes(gitLink[1]); + return <> +
+ + { + installed && + } +
+ ; } 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; +}