feat(plugin): UserpluginInstaller (#974)
Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com> Co-authored-by: creations <creations@creations.works>
This commit is contained in:
parent
8e19aecd92
commit
c0459e2adb
11 changed files with 6732 additions and 0 deletions
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 nin0
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
export class VariableWithCallbacks<T> {
|
||||
#value: T;
|
||||
#callbacks: {
|
||||
id: number;
|
||||
callback: (value: T, id: number) => void;
|
||||
}[] = [];
|
||||
|
||||
constructor(value: T) {
|
||||
this.#value = value;
|
||||
}
|
||||
|
||||
value(newValue?: T): T {
|
||||
if (newValue !== undefined) {
|
||||
this.#value = newValue;
|
||||
this.#callbacks.forEach(c => c.callback(this.#value, c.id));
|
||||
}
|
||||
return this.#value;
|
||||
}
|
||||
|
||||
registerCallback(callback: (value: T, id: number) => void): number {
|
||||
const id = Date.now();
|
||||
this.#callbacks.push({
|
||||
id,
|
||||
callback
|
||||
});
|
||||
return id;
|
||||
}
|
||||
|
||||
deregisterCallback(id: number) {
|
||||
const possibleFallback = this.#callbacks.find(cb => cb.id === id);
|
||||
if (!possibleFallback) return;
|
||||
this.#callbacks.splice(this.#callbacks.indexOf(possibleFallback), 1);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,340 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 nin0
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { pluginRequiresRestart, startPlugin, stopPlugin } from "@api/PluginManager";
|
||||
import { useSettings } from "@api/Settings";
|
||||
import { BaseText } from "@components/BaseText";
|
||||
import { Button } from "@components/Button";
|
||||
import { Card } from "@components/Card";
|
||||
import { CheckedTextInput } from "@components/CheckedTextInput";
|
||||
import { HeadingTertiary } from "@components/Heading";
|
||||
import { DeleteIcon } from "@components/Icons";
|
||||
import { Paragraph } from "@components/Paragraph";
|
||||
import { AddonCard } from "@components/settings";
|
||||
import {
|
||||
SettingsTab as STab,
|
||||
wrapTab,
|
||||
} from "@components/settings/tabs/BaseTab";
|
||||
import { classes, isObjectEmpty } from "@utils/misc";
|
||||
import { closeAllModals } from "@utils/modal";
|
||||
import { relaunch } from "@utils/native";
|
||||
import { Alerts, NavigationRouter, Toasts, useEffect, useState } from "@webpack/common";
|
||||
|
||||
import userpluginInstaller, { Native } from "..";
|
||||
import {
|
||||
cl,
|
||||
CLONE_LINK_REGEX,
|
||||
showInstallFinishedAlert,
|
||||
} from "../misc/constants";
|
||||
|
||||
function UserPluginsTab() {
|
||||
const rs = useSettings(["plugins.*"]);
|
||||
const [pluginsLoaded, loadPlugins] = useState(false);
|
||||
const [plugins, setPlugins] = useState<
|
||||
{
|
||||
name: string;
|
||||
description: string;
|
||||
usesPreSend: boolean;
|
||||
usesNative: boolean;
|
||||
directory?: string;
|
||||
remote: string;
|
||||
supportChannelID?: string;
|
||||
}[]
|
||||
>([]);
|
||||
const [url, setUrl] = useState("");
|
||||
const [valid, setValid] = useState(false);
|
||||
const [pluginsWithUpdates, setPluginsWithUpdates] = useState<
|
||||
Record<string, string>
|
||||
>({});
|
||||
const [updatesLoaded, loadUpdates] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const { plugins: plgobj, pluginsWithUpdates: pwug } =
|
||||
userpluginInstaller;
|
||||
setPlugins(plgobj.value());
|
||||
loadPlugins(true);
|
||||
const cid2 = plgobj.registerCallback(value => setPlugins(value));
|
||||
|
||||
const setPWU = value => {
|
||||
const pwu = value.plugins.map(pg => ({
|
||||
[pg]: plgobj.value().find(pfjh => pfjh.directory === pg)
|
||||
?.name,
|
||||
}));
|
||||
setPluginsWithUpdates(Object.assign({}, ...pwu));
|
||||
loadUpdates(value.finished);
|
||||
};
|
||||
const cid = pwug.registerCallback(value => {
|
||||
setPWU(value);
|
||||
});
|
||||
setPWU(pwug.value());
|
||||
|
||||
return () => {
|
||||
pwug.deregisterCallback(cid);
|
||||
plgobj.deregisterCallback(cid2);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<STab
|
||||
// @ts-ignore
|
||||
title={`UserPlugins${pluginsLoaded ? ` (${plugins.length}, ${plugins.filter(p => Vencord.Settings.plugins[p.name].enabled).length} enabled)` : ""}`}
|
||||
>
|
||||
<div className={cl("update-check-container")}>
|
||||
{isObjectEmpty(pluginsWithUpdates) ? (
|
||||
!updatesLoaded && (
|
||||
<BaseText>Checking for updates...</BaseText>
|
||||
)
|
||||
) : (
|
||||
<Card
|
||||
className={classes(cl("info-card"), "vc-warning-card")}
|
||||
>
|
||||
<HeadingTertiary className={cl("install-title")}>
|
||||
Plugin Updates Available
|
||||
</HeadingTertiary>
|
||||
<Paragraph className={cl("install-desc")}>
|
||||
The following plugins are out-of-date:
|
||||
<ul className={cl("outdated-list")}>
|
||||
{Object.values(pluginsWithUpdates)
|
||||
.toSorted()
|
||||
.map(pl => (
|
||||
<li key={pl}>{pl}</li>
|
||||
))}
|
||||
</ul>
|
||||
{!updatesLoaded &&
|
||||
"and possibly more, as update checking is not finished. "}
|
||||
You can update plugins below.
|
||||
</Paragraph>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
<Card className={cl("info-card")}>
|
||||
<HeadingTertiary className={cl("install-title")}>
|
||||
Install Plugin
|
||||
</HeadingTertiary>
|
||||
<Paragraph className={cl("install-desc")}>
|
||||
You can install a plugin from GitHub, GitLab, Codeberg,
|
||||
git.nin0.dev, or plugins.nin0.dev by pasting its clone URL
|
||||
here.
|
||||
</Paragraph>
|
||||
<div className={cl("install-field")}>
|
||||
<CheckedTextInput
|
||||
onChange={t => setUrl(t)}
|
||||
validate={t => {
|
||||
const match = t.match(CLONE_LINK_REGEX);
|
||||
if (match) {
|
||||
const idpl = match.includes("plugins.nin0.dev")
|
||||
? 1
|
||||
: 0;
|
||||
const installed = plugins
|
||||
.map(p => p.directory)
|
||||
.includes(match[[3, 6][idpl]]);
|
||||
if (installed) {
|
||||
setValid(false);
|
||||
return "Plugin already installed, update below";
|
||||
}
|
||||
setValid(true);
|
||||
return true;
|
||||
} else {
|
||||
setValid(false);
|
||||
return "Invalid URL, read the notice above";
|
||||
}
|
||||
}}
|
||||
value={url}
|
||||
/>
|
||||
</div>
|
||||
<div className={cl("button-container")}>
|
||||
<Button
|
||||
disabled={!valid}
|
||||
className={cl("install-button")}
|
||||
onClick={async () => {
|
||||
const gitLink = url.match(CLONE_LINK_REGEX)!;
|
||||
const idpl = gitLink.includes("plugins.nin0.dev")
|
||||
? 1
|
||||
: 0;
|
||||
try {
|
||||
const { name, native } = JSON.parse(
|
||||
await Native.initPluginInstall(
|
||||
gitLink[0],
|
||||
gitLink[[1, 4][idpl]],
|
||||
gitLink[[2, 5][idpl]],
|
||||
gitLink[[3, 6][idpl]],
|
||||
),
|
||||
);
|
||||
showInstallFinishedAlert(name, native);
|
||||
} catch (e: any) {
|
||||
if (e.toString().includes("silentStop")) return;
|
||||
Alerts.show({
|
||||
title: "Install error",
|
||||
body: e.toString(),
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Install
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
<div className={cl("plugins-container")}>
|
||||
{pluginsLoaded ? (
|
||||
<div className={cl("plugins-grid")}>
|
||||
{plugins
|
||||
.toSorted((a, b) =>
|
||||
a.name.localeCompare(b.name, "en", {
|
||||
sensitivity: "base",
|
||||
}),
|
||||
)
|
||||
.toSorted((a, b) => {
|
||||
const updatePendingNames =
|
||||
Object.keys(pluginsWithUpdates);
|
||||
const [aa, bb] = [
|
||||
updatePendingNames.includes(a.directory!)
|
||||
? 1000
|
||||
: 0,
|
||||
updatePendingNames.includes(b.directory!)
|
||||
? 1000
|
||||
: 0,
|
||||
];
|
||||
return bb - aa;
|
||||
})
|
||||
.map(plugin => {
|
||||
|
||||
const pl = Vencord.Plugins.plugins[
|
||||
plugin.name
|
||||
];
|
||||
return <AddonCard
|
||||
key={pl.name}
|
||||
name={pl.name}
|
||||
description={pl.description}
|
||||
enabled={rs.plugins[pl.name].enabled}
|
||||
infoButton={<button
|
||||
role="switch"
|
||||
onClick={async () => {
|
||||
await Native.rmPlugin(
|
||||
plugin.directory!,
|
||||
);
|
||||
Alerts.show({
|
||||
title: "Done!",
|
||||
body: `${plugin.name} has been uninstalled. A ${plugin.usesNative ? "restart" : "refresh"} is needed to fully remove the plugin.`,
|
||||
confirmText:
|
||||
plugin.usesNative
|
||||
? "Restart"
|
||||
: "Refresh",
|
||||
cancelText: "Later",
|
||||
onConfirm() {
|
||||
plugin.usesNative
|
||||
? relaunch()
|
||||
: window.location.reload();
|
||||
},
|
||||
});
|
||||
}}
|
||||
className={cl("delete-button")}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</button>}
|
||||
setEnabled={t => {
|
||||
Vencord.Settings.plugins[pl.name].enabled = t;
|
||||
if (pluginRequiresRestart(pl)) {
|
||||
Toasts.show({
|
||||
id: Toasts.genId(),
|
||||
message: "Restart to apply changes!",
|
||||
type: Toasts.Type.MESSAGE
|
||||
});
|
||||
} else {
|
||||
(t ? startPlugin : stopPlugin)(pl);
|
||||
}
|
||||
}}
|
||||
footer={(
|
||||
<div className={cl("plugin-footer")}>
|
||||
{Object.keys(
|
||||
pluginsWithUpdates,
|
||||
).includes(plugin.directory!) && (
|
||||
<Button
|
||||
size="small"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await Native.updatePlugin(
|
||||
plugin.directory!,
|
||||
);
|
||||
const oldPWU =
|
||||
userpluginInstaller.pluginsWithUpdates.value()
|
||||
.plugins;
|
||||
oldPWU.splice(
|
||||
oldPWU.indexOf(
|
||||
plugin.directory!,
|
||||
),
|
||||
1,
|
||||
);
|
||||
userpluginInstaller.pluginsWithUpdates.value(
|
||||
{
|
||||
finished: true,
|
||||
plugins: oldPWU,
|
||||
},
|
||||
);
|
||||
Alerts.show({
|
||||
title: "Done!",
|
||||
body: `${plugin.name} has been updated. A ${plugin.usesNative ? "restart" : "refresh"} is needed to apply the update.`,
|
||||
confirmText:
|
||||
plugin.usesNative
|
||||
? "Restart"
|
||||
: "Refresh",
|
||||
cancelText: "Later",
|
||||
onConfirm() {
|
||||
plugin.usesNative
|
||||
? relaunch()
|
||||
: window.location.reload();
|
||||
},
|
||||
});
|
||||
} catch (e: any) {
|
||||
if (
|
||||
e
|
||||
.toString()
|
||||
.includes(
|
||||
"silentStop",
|
||||
)
|
||||
)
|
||||
return;
|
||||
Alerts.show({
|
||||
title: "Update error",
|
||||
body: e.toString(),
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
disabled={plugin.remote === ""}
|
||||
onClick={() =>
|
||||
VencordNative.native.openExternal(
|
||||
plugin.remote,
|
||||
)
|
||||
}
|
||||
>
|
||||
Source
|
||||
</Button>
|
||||
{plugin.supportChannelID && <Button variant="secondary" size="small" onClick={() => {
|
||||
closeAllModals();
|
||||
NavigationRouter.transitionTo(`/channels/1015060230222131221/${plugin.supportChannelID}`);
|
||||
}}>
|
||||
Support
|
||||
</Button>}
|
||||
</div>
|
||||
)}
|
||||
/>;
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<BaseText>Loading plugins...</BaseText>
|
||||
)}
|
||||
</div>
|
||||
</STab>
|
||||
);
|
||||
}
|
||||
|
||||
export default wrapTab(UserPluginsTab, "UserPlugins");
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 nin0
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Button } from "@components/Button";
|
||||
import { Alerts, ChannelStore, useEffect, useState } from "@webpack/common";
|
||||
|
||||
import userpluginInstaller, { Native, OpenSettingsModule, settings } from "..";
|
||||
import { CLONE_LINK_REGEX, showInstallFinishedAlert, WHITELISTED_SHARE_CHANNELS } from "../misc/constants";
|
||||
|
||||
export default function UserpluginInstallButton({ props }: any) {
|
||||
const [plugins, setPlugins] = useState<{
|
||||
directory?: string;
|
||||
}[]>([]);
|
||||
useEffect(() => {
|
||||
const { plugins } = userpluginInstaller;
|
||||
|
||||
setPlugins(plugins.value());
|
||||
const cid = plugins.registerCallback(value => setPlugins(plugins.value()));
|
||||
return () => plugins.deregisterCallback(cid);
|
||||
});
|
||||
const { message } = props;
|
||||
if (![...WHITELISTED_SHARE_CHANNELS, ...(settings.store.allowlistedChannels || "").split(",")].includes(ChannelStore.getChannel(message.channel_id).parent_id) && !WHITELISTED_SHARE_CHANNELS.includes(message.channel_id))
|
||||
return;
|
||||
const gitLink = (props.message.content as string).match(CLONE_LINK_REGEX);
|
||||
if (!gitLink) return;
|
||||
const idpl = gitLink.includes("plugins.nin0.dev") ? 1 : 0;
|
||||
const installed = plugins.map(p => p.directory).includes(gitLink[[3, 6][idpl]]);
|
||||
return <>
|
||||
<div style={{ display: "flex" }}>
|
||||
<Button style={{
|
||||
marginTop: "5px"
|
||||
}}
|
||||
variant={installed ? "secondary" : "primary"}
|
||||
onClick={async () => {
|
||||
if (installed) return void OpenSettingsModule.openUserSettings("vencord_userplugins_panel");
|
||||
try {
|
||||
const { name, native } = JSON.parse(await Native.initPluginInstall(gitLink[0], gitLink[[1, 4][idpl]], gitLink[[2, 5][idpl]], gitLink[[3, 6][idpl]]));
|
||||
showInstallFinishedAlert(name, native);
|
||||
}
|
||||
catch (e: any) {
|
||||
if (e.toString().includes("silentStop")) return;
|
||||
Alerts.show({
|
||||
title: "Install error",
|
||||
body: e.toString()
|
||||
});
|
||||
}
|
||||
}}>
|
||||
{installed ? "Manage plugins" : "Install plugin"}
|
||||
</Button>
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
138
src/equicordplugins/userpluginInstaller.dev/index.tsx
Normal file
138
src/equicordplugins/userpluginInstaller.dev/index.tsx
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 nin0
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./misc/style.css";
|
||||
|
||||
import { showNotification } from "@api/Notifications";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Button } from "@components/Button";
|
||||
import plSettings from "@plugins/_core/settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { relaunch } from "@utils/native";
|
||||
import definePlugin, { OptionType, PluginNative } from "@utils/types";
|
||||
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
|
||||
import { Alerts } from "@webpack/common";
|
||||
|
||||
import SettingsTab from "./components/SettingsTab";
|
||||
import UserpluginInstallButton from "./components/UserpluginInstallButton";
|
||||
import { VariableWithCallbacks } from "./VariableWithCallbacks";
|
||||
|
||||
// @ts-ignore
|
||||
export const Native = VencordNative.pluginHelpers.UserpluginInstaller as PluginNative<typeof import("./native")>;
|
||||
export const OpenSettingsModule = findByPropsLazy("openUserSettings");
|
||||
const AppsIcon = findComponentByCodeLazy("2.95H20a2 2 0");
|
||||
|
||||
export const settings = definePluginSettings({
|
||||
allowlistedChannels: {
|
||||
type: OptionType.STRING,
|
||||
description: "Comma separated list of channels where the Install Plugin button should be displayed. It is always displayed in the Vencord Userplugin channels"
|
||||
},
|
||||
notifyIfUpdate: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Show a Vencord notification if UserPlugins need to be updated",
|
||||
default: true
|
||||
},
|
||||
neverNotifyForPlugins: {
|
||||
type: OptionType.STRING,
|
||||
description: "Never show update notifications for these plugins (you can still update them from the UserPlugins tab)",
|
||||
default: ""
|
||||
},
|
||||
setGitPath: {
|
||||
type: OptionType.COMPONENT,
|
||||
component: () => <Button onClick={() => {
|
||||
Native.openGitPathModal();
|
||||
}} variant="secondary">
|
||||
Set Git path
|
||||
</Button>
|
||||
}
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "UserpluginInstaller",
|
||||
description: "Install userplugins with a simple button click",
|
||||
async checkPluginUpdates() {
|
||||
for (const p of this.plugins.value()) {
|
||||
if (await Native.isUpdateAvailableForPlugin(p.directory!)) {
|
||||
const t = this.pluginsWithUpdates.value().plugins;
|
||||
t.push(p.directory!);
|
||||
this.pluginsWithUpdates.value({
|
||||
finished: false,
|
||||
plugins: t
|
||||
});
|
||||
}
|
||||
}
|
||||
const t = this.pluginsWithUpdates.value().plugins;
|
||||
this.pluginsWithUpdates.value({
|
||||
finished: true,
|
||||
plugins: t
|
||||
});
|
||||
},
|
||||
section: {
|
||||
key: "vencord_userplugins",
|
||||
title: "UserPlugins",
|
||||
panelTitle: "UserPlugins",
|
||||
Component: SettingsTab,
|
||||
Icon: AppsIcon
|
||||
},
|
||||
async start() {
|
||||
if (!VencordNative.pluginHelpers.UserpluginInstaller) return void Alerts.show({
|
||||
title: "UserpluginInstaller not fully loaded",
|
||||
body: "This usually happens if you didn't fully restart your client after installing the plugin, which prevents the native helpers from loading. Restart your client to fix the issue.",
|
||||
confirmText: "Restart now",
|
||||
onConfirm() {
|
||||
relaunch();
|
||||
},
|
||||
cancelText: "Later"
|
||||
});
|
||||
|
||||
plSettings.customEntries.push(this.section);
|
||||
|
||||
this.pluginsWithUpdates.registerCallback((value, id) => {
|
||||
if (value.plugins.length === 0) return;
|
||||
if (settings.store.neverNotifyForPlugins.split(",").map(t => t.trim().toLowerCase()).includes(value.plugins[value.plugins.length - 1].toLowerCase()))
|
||||
return;
|
||||
this.pluginsWithUpdates.deregisterCallback(id);
|
||||
if (settings.store.notifyIfUpdate)
|
||||
showNotification({
|
||||
title: "Some UserPlugins are out of date!",
|
||||
body: "Click to open the UserPlugin Updater",
|
||||
noPersist: true,
|
||||
permanent: true,
|
||||
onClick() {
|
||||
OpenSettingsModule.openUserSettings("vencord_userplugins_panel");
|
||||
},
|
||||
});
|
||||
});
|
||||
const pls = await Native.getUserplugins();
|
||||
// @ts-ignore :trolley:
|
||||
this.plugins.value(pls);
|
||||
await this.checkPluginUpdates();
|
||||
},
|
||||
stop() {
|
||||
// @ts-ignore
|
||||
plSettings.customEntries.splice(plSettings.customEntries.indexOf(this.section), 1);
|
||||
},
|
||||
plugins: new VariableWithCallbacks<{
|
||||
name: string;
|
||||
description: string;
|
||||
usesPreSend: boolean;
|
||||
usesNative: boolean;
|
||||
directory: string;
|
||||
remote: string;
|
||||
}[]>([]),
|
||||
pluginsWithUpdates: new VariableWithCallbacks<{
|
||||
finished: boolean;
|
||||
plugins: string[];
|
||||
}>({
|
||||
finished: false,
|
||||
plugins: []
|
||||
}),
|
||||
settings,
|
||||
authors: [Devs.nin0dev],
|
||||
renderMessageAccessory: props => {
|
||||
return <UserpluginInstallButton props={props} />;
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 nin0
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { relaunch } from "@utils/native";
|
||||
import { Alerts } from "@webpack/common";
|
||||
|
||||
// if edited, also edit in native.ts!!!
|
||||
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)?|(plugins\.(nin0)\.dev)\/((?:[a-zA-Z0-9]|-|\.)+))(?:\/)?/;
|
||||
export const WHITELISTED_SHARE_CHANNELS = ["1256395889354997771", "1032200195582197831", "1301947896601509900", "1322935137591365683"];
|
||||
export const cl = classNameFactory("vc-userplugininstaller-");
|
||||
|
||||
export function showInstallFinishedAlert(pluginToEnable: string, native: boolean) {
|
||||
Alerts.show({
|
||||
title: "Done!",
|
||||
body: `${pluginToEnable} has been successfully installed.${native ? " However, as it makes use of native functions, a client restart is required." : ""} What now?`,
|
||||
confirmText: `Enable & ${native ? "restart" : "refresh"}`,
|
||||
cancelText: native ? "Restart" : "Refresh",
|
||||
onConfirm() {
|
||||
!Vencord.Plugins.plugins[pluginToEnable] ? Vencord.Settings.plugins[pluginToEnable] = { enabled: true } : Vencord.Settings.plugins[pluginToEnable].enabled = true;
|
||||
native ? relaunch() : window.location.reload();
|
||||
},
|
||||
onCancel: () => native ? relaunch() : window.location.reload()
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Review userplugin</title>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
document
|
||||
.querySelector("#abort")
|
||||
.addEventListener("click", () => {
|
||||
document.title = "abortInstall";
|
||||
});
|
||||
document
|
||||
.querySelector("#install")
|
||||
.addEventListener("click", () => {
|
||||
if (
|
||||
!document
|
||||
.querySelector("style")
|
||||
.innerHTML.includes(
|
||||
"#native-ts-warning { display: none !important; }"
|
||||
) &&
|
||||
!document.querySelector("input[type='checkbox']")
|
||||
.checked
|
||||
)
|
||||
return alert(
|
||||
"Make sure to acknowledge all warnings before installing."
|
||||
);
|
||||
document.title = "install";
|
||||
});
|
||||
|
||||
document.querySelector("style").innerHTML +=
|
||||
document.querySelector("addAtRuntimeStyle").innerHTML;
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
addAtRuntimeStyle {
|
||||
display: none !important;
|
||||
}
|
||||
* {
|
||||
font-family: sans-serif;
|
||||
color: #cdd6f4;
|
||||
}
|
||||
body {
|
||||
background-color: #1e1e2e;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.card {
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #9399b2;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 10px; /* Added margin for better spacing */
|
||||
}
|
||||
.warn-card {
|
||||
border-color: #f38ba8;
|
||||
background-color: #313244;
|
||||
}
|
||||
.danger-card {
|
||||
border-color: #f38ba8;
|
||||
background-color: #313244;
|
||||
h3 {
|
||||
color: #cdd6f4;
|
||||
}
|
||||
}
|
||||
.card * {
|
||||
margin: 0;
|
||||
}
|
||||
.card h3 {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
input[type="checkbox"] {
|
||||
margin-right: 5px;
|
||||
}
|
||||
#validate {
|
||||
color: #f38ba8;
|
||||
}
|
||||
#btn-row {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
#btn-row * {
|
||||
flex: 1;
|
||||
margin: 5px;
|
||||
padding: 10px 5px;
|
||||
border: 1px solid #cdd6f4;
|
||||
border-radius: 5px;
|
||||
}
|
||||
#abort {
|
||||
background-color: #f38ba8;
|
||||
color: #11111b;
|
||||
}
|
||||
#install {
|
||||
color: #a6e3a1;
|
||||
border-color: #a6e3a1;
|
||||
background-color: #00000000;
|
||||
}
|
||||
#abort:hover {
|
||||
background-color: #ec9393;
|
||||
font-weight: bold;
|
||||
}
|
||||
#install:hover {
|
||||
background-color: #425040;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
<addAtRuntimeStyle>
|
||||
%WARNINGHIDER% %NATIVETSHIDER% %PRESENDHIDER%
|
||||
</addAtRuntimeStyle>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Plugin info</h2>
|
||||
<div class="card" id="plugin-info-card">
|
||||
<h3>%PLUGINNAME%</h3>
|
||||
<p>%PLUGINDESC%</p>
|
||||
</div>
|
||||
|
||||
<h2 data-useless="warning">Warnings</h2>
|
||||
<div
|
||||
data-useless="warning"
|
||||
class="card danger-card"
|
||||
id="native-ts-warning"
|
||||
>
|
||||
<h3>Uses a native.ts file</h3>
|
||||
<p>
|
||||
Use of this file allows the plugin to escape the browser sandbox
|
||||
and potentially do anything to your system and data.
|
||||
<b
|
||||
>ONLY INSTALL THIS PLUGIN AFTER REVIEWING THE CODE AND BEING
|
||||
100% SURE THAT NOTHING BAD CAN BE DONE!</b
|
||||
>
|
||||
</p>
|
||||
<br />
|
||||
<input type="checkbox" /> Acknowledge warning (required to allow
|
||||
install)
|
||||
</div>
|
||||
<div
|
||||
data-useless="warning"
|
||||
class="card warn-card"
|
||||
id="pre-send-warning"
|
||||
>
|
||||
<h3>Has pre-send listeners</h3>
|
||||
<p>
|
||||
This allows the plugin to edit your messages before they are
|
||||
sent.
|
||||
</p>
|
||||
</div>
|
||||
<p id="validate">
|
||||
Reminder: installing a userplugin can be a destructive action. Make
|
||||
sure that you know and trust the developer before installing any.
|
||||
</p>
|
||||
<div id="btn-row">
|
||||
<button id="abort">Cancel installation</button>
|
||||
<button id="install">Install plugin</button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
118
src/equicordplugins/userpluginInstaller.dev/misc/setGitPath.txt
Normal file
118
src/equicordplugins/userpluginInstaller.dev/misc/setGitPath.txt
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Git Path</title>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
document.title
|
||||
document.querySelector("#abort").addEventListener("click", () => {document.title = "abort";})
|
||||
document.querySelector("#check").addEventListener("click", () => {document.title = `check-${document.querySelector("input").value.trim()}`; setTimeout(() => {document.title = "git";}, 1000)})
|
||||
document.querySelector("#install").addEventListener("click", () => {document.title = `ok-${document.querySelector("input").value.trim()}`;})
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
* {
|
||||
font-family: sans-serif;
|
||||
color: #cdd6f4;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #1e1e2e;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#btn-row {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#btn-row * {
|
||||
flex: 1;
|
||||
margin: 5px;
|
||||
padding: 10px 5px;
|
||||
border: 1px solid #cdd6f4;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#abort {
|
||||
border-color: #f38ba8;
|
||||
color: #f38ba8;
|
||||
background-color: #00000000;
|
||||
}
|
||||
|
||||
#install {
|
||||
color: #a6e3a1;
|
||||
border-color: #a6e3a1;
|
||||
background-color: #00000000;
|
||||
}
|
||||
|
||||
#check {
|
||||
color: #a1afe3;
|
||||
border-color: #a1afe3;
|
||||
background-color: #00000000;
|
||||
}
|
||||
|
||||
#abort:hover {
|
||||
background-color: #504040;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#check:hover {
|
||||
background-color: #404550;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#install:hover {
|
||||
background-color: #425040;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
input {
|
||||
background-color: #313244;
|
||||
color: #cdd6f4;
|
||||
padding: 5px 10px;
|
||||
margin-bottom: 15px;
|
||||
border: 1px solid #6c7086;
|
||||
border-radius: 5px;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border: 2px solid #cdd6f4;
|
||||
padding: 4px 9px;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: #6c7086;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Git path</h2>
|
||||
<p>
|
||||
If you're having issues downloading userplugins because Git isn't properly detected, you can manually set a Git
|
||||
path
|
||||
that'll be used instead of
|
||||
simply looking in PATH.
|
||||
<br />
|
||||
If you do not specify anything, Git will be called from the PATH variable. Do not set this setting unless you
|
||||
have issues.
|
||||
<br />
|
||||
<br />
|
||||
On Linux, Flatpak still isn't supported.
|
||||
</p>
|
||||
<input type="text" id="git-path" placeholder="Full, absolute path to Git" />
|
||||
<div id="btn-row">
|
||||
<button id="abort">Cancel</button>
|
||||
<button id="check">Check</button>
|
||||
<button id="install">Set</button>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
78
src/equicordplugins/userpluginInstaller.dev/misc/style.css
Normal file
78
src/equicordplugins/userpluginInstaller.dev/misc/style.css
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
.vc-userplugininstaller-info-card {
|
||||
padding: 15px 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.vc-userplugininstaller-install-desc {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.vc-userplugininstaller-install-field,
|
||||
.vc-userplugininstaller-button-container {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.vc-userplugininstaller-install-button {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.vc-userplugininstaller-update-check-container {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.vc-userplugininstaller-plugins-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.vc-userplugininstaller-info-button {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: var(--icon-secondary);
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--interactive-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.vc-userplugininstaller-settings-button:hover {
|
||||
color: var(--interactive-hover);
|
||||
}
|
||||
|
||||
.vc-userplugininstaller-plugin-footer {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
|
||||
div {
|
||||
flex-basis: 100%;
|
||||
margin-top: 5px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
.vc-userplugininstaller-outdated-list {
|
||||
padding-left: 30px;
|
||||
margin: 3px 0;
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
.vc-userplugininstaller-delete-button {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
margin-right: 8px;
|
||||
color: var(--icon-muted);
|
||||
transition: background-color .2s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--red-new-46)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Review userplugin</title>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
document
|
||||
.querySelector("#abort")
|
||||
.addEventListener("click", () => {
|
||||
document.title = "abortInstall";
|
||||
});
|
||||
document
|
||||
.querySelector("#install")
|
||||
.addEventListener("click", () => {
|
||||
document.title = "install";
|
||||
});
|
||||
|
||||
for (const aEl of document.querySelectorAll("a")) {
|
||||
aEl.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
document.title = `openLink:${aEl.href}`;
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
addAtRuntimeStyle {
|
||||
display: none !important;
|
||||
}
|
||||
* {
|
||||
font-family: sans-serif;
|
||||
color: #cdd6f4;
|
||||
}
|
||||
body {
|
||||
background-color: #1e1e2e;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.card {
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #9399b2;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 10px; /* Added margin for better spacing */
|
||||
}
|
||||
.warn-card {
|
||||
border-color: #f38ba8;
|
||||
background-color: #313244;
|
||||
}
|
||||
.danger-card {
|
||||
border-color: #f38ba8;
|
||||
background-color: #313244;
|
||||
h3 {
|
||||
color: #cdd6f4;
|
||||
}
|
||||
}
|
||||
.card * {
|
||||
margin: 0;
|
||||
}
|
||||
.card h3 {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
input[type="checkbox"] {
|
||||
margin-right: 5px;
|
||||
}
|
||||
#validate {
|
||||
color: #f38ba8;
|
||||
}
|
||||
#btn-row {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
#btn-row * {
|
||||
flex: 1;
|
||||
margin: 5px;
|
||||
padding: 10px 5px;
|
||||
border: 1px solid #cdd6f4;
|
||||
border-radius: 5px;
|
||||
}
|
||||
#abort {
|
||||
background-color: #f38ba8;
|
||||
color: #11111b;
|
||||
}
|
||||
#install {
|
||||
color: #a6e3a1;
|
||||
border-color: #a6e3a1;
|
||||
background-color: #00000000;
|
||||
}
|
||||
#abort:hover {
|
||||
background-color: #ec9393;
|
||||
font-weight: bold;
|
||||
}
|
||||
#install:hover {
|
||||
background-color: #425040;
|
||||
font-weight: bold;
|
||||
}
|
||||
a {
|
||||
color: #89b4fa;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Plugin info</h2>
|
||||
<div class="card" id="plugin-info-card">
|
||||
<h3>%PLUGINNAME%</h3>
|
||||
<p>%PLUGINDESC%</p>
|
||||
</div>
|
||||
<h2>Update info</h2>
|
||||
<div class="card" id="update-info-card">
|
||||
<p>%COMMITMESSAGE%</p>
|
||||
<br />
|
||||
<p>
|
||||
You may want to review the
|
||||
<a href="%REMOTE%">source code</a> that will be applied.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p id="validate">
|
||||
Reminder: installing a userplugin can be a destructive action. Make
|
||||
sure that you know and trust the developer before installing any.
|
||||
</p>
|
||||
<div id="btn-row">
|
||||
<button id="abort">Cancel update</button>
|
||||
<button id="install">Apply update</button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
473
src/equicordplugins/userpluginInstaller.dev/native.ts
Normal file
473
src/equicordplugins/userpluginInstaller.dev/native.ts
Normal file
|
|
@ -0,0 +1,473 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 nin0
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { NativeSettings } from "@main/settings";
|
||||
import { exec, spawn } from "child_process";
|
||||
import { BrowserWindow, dialog, shell, WebContentsView } from "electron";
|
||||
import { existsSync, readdirSync, readFileSync } from "fs";
|
||||
import { readdir, readFile, rm } from "fs/promises";
|
||||
import { basename, join } from "path";
|
||||
|
||||
// @ts-ignore fuck off
|
||||
import pluginValidateContent from "./misc/pluginValidate.txt"; // i would use HTML but esbuild is being whiny
|
||||
// @ts-ignore fuck off
|
||||
import setGitPathContent from "./misc/setGitPath.txt";
|
||||
// @ts-ignore fuck off
|
||||
import updateValidateContent from "./misc/updateValidate.txt"; // see above
|
||||
import YAML from "./yaml.js";
|
||||
|
||||
const PLUGIN_META_REGEX = /export default definePlugin\((?:\s|\/(?:\/|\*).*)*{\s*(?:\s|\/(?:\/|\*).*)*name:\s*(?:"|'|`)(.*)(?:"|'|`)(?:\s|\/(?:\/|\*).*)*,(?:\s|\/(?:\/|\*).*)*(?:\s|\/(?:\/|\*).*)*description:\s*(?:"|'|`)(.*)(?:"|'|`)(?:\s|\/(?:\/|\*).*)*/;
|
||||
// if edited, also edit in misc/constants.ts!!!
|
||||
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)?|(plugins\.(nin0)\.dev)\/((?:[a-zA-Z0-9]|-|\.)+))(?:\/)?/;
|
||||
|
||||
const vencordPath = ["desktop", "equibop"].includes(basename(__dirname)) ? join(__dirname, "../") : __dirname;
|
||||
|
||||
export async function rmPlugin(_, name: string): Promise<string> {
|
||||
// eslint-disable-next-line
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const ups = await getUserplugins();
|
||||
const pl = ups.find(p => p.directory! === name);
|
||||
if (!pl) return;
|
||||
|
||||
const deleteReqDialog = await dialog.showMessageBox({
|
||||
title: "Uninstall plugin",
|
||||
message: `Uninstall ${pl.name}`,
|
||||
type: "error",
|
||||
detail: `The uninstall of the userplugin ${pl.name} has been requested. Would you like to do so?\n\nIf you did not initiate this, press No.`,
|
||||
buttons: ["No", "Yes"]
|
||||
});
|
||||
|
||||
if (deleteReqDialog.response !== 1) return reject("User rejected");
|
||||
await rm(join(vencordPath, "../src/userplugins", name), { recursive: true });
|
||||
|
||||
await build();
|
||||
resolve("Done");
|
||||
});
|
||||
}
|
||||
|
||||
export async function isUpdateAvailableForPlugin(_, name: string): Promise<boolean> {
|
||||
return new Promise(resolve => {
|
||||
const pluginDir = join(vencordPath, "../src/userplugins", name);
|
||||
const otherProc = exec("git fetch", {
|
||||
cwd: pluginDir
|
||||
});
|
||||
otherProc.once("close", () => {
|
||||
async function doStuff() {
|
||||
try {
|
||||
const head = (await readFile(join(pluginDir, ".git/HEAD"), "utf8")).match(/^ref: (.+)/)![1];
|
||||
const remoteHead = (await readFile(join(pluginDir, ".git/refs/remotes/origin/HEAD"), "utf8")).match(/^ref: (.+)/)![1];
|
||||
const localCommit = await readFile(join(pluginDir, ".git", head), "utf8");
|
||||
const remoteCommit = await readFile(join(pluginDir, ".git", remoteHead), "utf8");
|
||||
|
||||
resolve(localCommit !== remoteCommit);
|
||||
}
|
||||
catch (e) {
|
||||
resolve(false);
|
||||
}
|
||||
}
|
||||
doStuff();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function initPluginInstall(_, link: string, source: string, owner: string, repo: string): Promise<string> {
|
||||
// eslint-disable-next-line
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const verifiedRegex = link.match(CLONE_LINK_REGEX)!;
|
||||
const idpl = source === "plugins.nin0.dev" ? 1 : 0;
|
||||
if (![4, 7].includes(verifiedRegex.length) || verifiedRegex[0] !== link || verifiedRegex[[1, 4][idpl]] !== source || verifiedRegex[[2, 5][idpl]] !== owner || verifiedRegex[[3, 6][idpl]] !== repo) return reject("Invalid link");
|
||||
|
||||
// Ask for clone
|
||||
const cloneDialog = await dialog.showMessageBox({
|
||||
title: "Clone userplugin",
|
||||
message: `You are about to clone a userplugin from ${source}.`,
|
||||
type: "question",
|
||||
detail: `The repository name is "${repo}" and it is owned by "${owner}".\nThe repository URL is ${link}\n\n(If you did not request this intentionally, choose Cancel)`,
|
||||
buttons: ["Cancel", "Clone repository and continue install", "Open repository in browser"]
|
||||
});
|
||||
switch (cloneDialog.response) {
|
||||
case 0: {
|
||||
return reject("Rejected by user");
|
||||
}
|
||||
case 1: {
|
||||
await cloneRepo(link, repo);
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
await shell.openExternal(link);
|
||||
return reject("silentStop");
|
||||
}
|
||||
}
|
||||
|
||||
// Get plugin meta
|
||||
const meta = await getPluginMeta(join(vencordPath, "..", "src", "userplugins", repo));
|
||||
|
||||
// Review plugin
|
||||
const win = new BrowserWindow({
|
||||
maximizable: false,
|
||||
minimizable: false,
|
||||
width: 560,
|
||||
height: meta.usesNative || meta.usesPreSend ? 650 : 360,
|
||||
resizable: false,
|
||||
webPreferences: {
|
||||
devTools: true
|
||||
},
|
||||
title: "Review userplugin",
|
||||
modal: true,
|
||||
parent: BrowserWindow.getAllWindows()[0],
|
||||
show: false,
|
||||
autoHideMenuBar: true
|
||||
});
|
||||
const reView /* haha got it */ = new WebContentsView({
|
||||
webPreferences: {
|
||||
devTools: true,
|
||||
nodeIntegration: true
|
||||
}
|
||||
});
|
||||
win.contentView.addChildView(reView);
|
||||
win.loadURL(generateReviewPluginContent(meta));
|
||||
win.on("page-title-updated", async e => {
|
||||
switch (win.webContents.getTitle() as "abortInstall" | "reviewCode" | "install") {
|
||||
case "abortInstall": {
|
||||
win.close();
|
||||
await rm(join(vencordPath, "..", "src", "userplugins", repo), {
|
||||
recursive: true
|
||||
});
|
||||
return reject("Rejected by user");
|
||||
}
|
||||
case "install": {
|
||||
win.close();
|
||||
try {
|
||||
await build();
|
||||
}
|
||||
catch (e) {
|
||||
reject((e as Error).toString());
|
||||
}
|
||||
resolve(JSON.stringify({
|
||||
name: meta.name,
|
||||
native: meta.usesNative
|
||||
}));
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
win.show();
|
||||
});
|
||||
}
|
||||
|
||||
async function build(): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const proc = exec("pnpm build --dev", {
|
||||
cwd: join(vencordPath, ".."),
|
||||
shell: process.env.SHELL || process.env.ComSpec || "/bin/sh"
|
||||
});
|
||||
proc.once("close", () => {
|
||||
if (proc.exitCode !== 0) {
|
||||
reject("Failed to build Vencord, try building from console");
|
||||
}
|
||||
resolve("Success");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function getPluginMeta(path: string, extra: object = {}): Promise<{
|
||||
name: string;
|
||||
description: string;
|
||||
usesPreSend: boolean;
|
||||
usesNative: boolean;
|
||||
directory?: string;
|
||||
remote: string;
|
||||
supportChannelID?: string;
|
||||
}> {
|
||||
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("Invalid plugin");
|
||||
|
||||
const file = readFileSync(`${path}/${fileToRead}`, "utf8");
|
||||
let remoteURL;
|
||||
try {
|
||||
const remoteC = readFileSync(join(path, ".git/config"), "utf8");
|
||||
remoteURL = remoteC.match(/\[remote "origin"]\s+url = (https:\/\/(?:(?:git(?:hub|lab)\.com|git\.(?:[a-zA-Z0-9]|\.)+|codeberg\.org)\/(?!user-attachments)(?:[a-zA-Z0-9]|-)+\/(?:[a-zA-Z0-9]|-|\.)+(?:\.git)?|(plugins\.(nin0)\.dev)\/((?:[a-zA-Z0-9]|-|\.)+))(?:\/)?)\n/);
|
||||
} catch {
|
||||
remoteURL = null;
|
||||
}
|
||||
|
||||
let supportChannelID;
|
||||
try {
|
||||
const meta = readFileSync(join(path, "meta.yml"), "utf8");
|
||||
console.log(meta);
|
||||
const parsed = YAML.load(meta);
|
||||
if (parsed.thread && typeof parsed.thread === "string" && /^\d+$/.test(parsed.thread)) {
|
||||
supportChannelID = parsed.thread;
|
||||
}
|
||||
} catch {
|
||||
supportChannelID = null;
|
||||
}
|
||||
|
||||
const rawMeta = file.match(PLUGIN_META_REGEX);
|
||||
resolve({
|
||||
name: rawMeta![1],
|
||||
description: rawMeta![2],
|
||||
usesPreSend: file.includes("PreSendListener") || file.includes("onBeforeMessage"),
|
||||
usesNative: files.includes("native.ts") || files.includes("native.js"),
|
||||
remote: remoteURL ? remoteURL[1] : "",
|
||||
supportChannelID,
|
||||
...extra
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
async function cloneRepo(link: string, repo: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const proc = spawn("git", ["clone", link], {
|
||||
cwd: join(vencordPath, "..", "src", "userplugins")
|
||||
});
|
||||
proc.once("close", async () => {
|
||||
if (proc.exitCode !== 0) {
|
||||
if (!existsSync(join(vencordPath, "..", "src", "userplugins", repo)))
|
||||
return reject("Failed to clone");
|
||||
const deleteReqDialog = await dialog.showMessageBox({
|
||||
title: "Error",
|
||||
message: "Plugin already exists",
|
||||
type: "error",
|
||||
detail: `The plugin that you tried to clone already exists at ${join(vencordPath, "..", "src", "userplugins")}.\nWould you like to reclone it? Only do this if you want to reinstall or update the plugin.`,
|
||||
buttons: ["No", "Yes"]
|
||||
});
|
||||
if (deleteReqDialog.response !== 1) return reject("User rejected");
|
||||
await rm(join(vencordPath, "..", "src", "userplugins", repo), {
|
||||
recursive: true
|
||||
});
|
||||
await cloneRepo(link, repo);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function generateReviewPluginContent(meta: {
|
||||
name: string;
|
||||
description: string;
|
||||
usesPreSend: boolean;
|
||||
usesNative: boolean;
|
||||
}): string {
|
||||
const template = pluginValidateContent.replace("%PLUGINNAME%", meta.name.replaceAll("<", "<")).replace("%PLUGINDESC%", meta.description.replaceAll("<", "<")).replace("%WARNINGHIDER%", !meta.usesNative && !meta.usesPreSend ? "[data-useless=\"warning\"] { display: none !important; }" : "").replace("%NATIVETSHIDER%", meta.usesNative ? "" : "#native-ts-warning { display: none !important; }").replace("%PRESENDHIDER%", meta.usesPreSend ? "" : "#pre-send-warning { display: none !important; }");
|
||||
const buf = Buffer.from(template).toString("base64");
|
||||
return `data:text/html;base64,${buf}`;
|
||||
}
|
||||
|
||||
function generateUpdatePluginContent(meta: {
|
||||
name: string;
|
||||
description: string;
|
||||
remote: string;
|
||||
commit: string;
|
||||
}): string {
|
||||
const template = updateValidateContent.replace("%PLUGINNAME%", meta.name.replaceAll("<", "<")).replace("%PLUGINDESC%", meta.description.replaceAll("<", "<")).replace("%REMOTE%", meta.remote).replace("%COMMITMESSAGE%", meta.commit.replaceAll("\n", "<br />"));
|
||||
const buf = Buffer.from(template).toString("base64");
|
||||
return `data:text/html;base64,${buf}`;
|
||||
}
|
||||
|
||||
export async function getUserplugins() {
|
||||
const folderContents = await readdir(join(vencordPath, "..", "src", "userplugins"), {
|
||||
withFileTypes: true
|
||||
});
|
||||
const plugins = await Promise.allSettled(
|
||||
folderContents
|
||||
.filter(item => item.isDirectory())
|
||||
.map(item => ({
|
||||
path: join(item.parentPath, item.name),
|
||||
directory: item.name
|
||||
}))
|
||||
.map(({ path, directory }) => getPluginMeta(path, { directory }))
|
||||
);
|
||||
|
||||
return plugins
|
||||
.filter(p => p.status === "fulfilled")
|
||||
.map(p => p.value);
|
||||
}
|
||||
|
||||
export async function updatePlugin(_, directory: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const pluginDir = join(vencordPath, "../src/userplugins", directory);
|
||||
|
||||
async function doStuff() {
|
||||
const pluginMeta = await getPluginMeta(pluginDir);
|
||||
|
||||
const win = new BrowserWindow({
|
||||
maximizable: false,
|
||||
minimizable: false,
|
||||
width: 560,
|
||||
height: 600,
|
||||
resizable: false,
|
||||
webPreferences: {
|
||||
devTools: true
|
||||
},
|
||||
title: "Review userplugin",
|
||||
modal: true,
|
||||
parent: BrowserWindow.getAllWindows()[0],
|
||||
show: false,
|
||||
autoHideMenuBar: true
|
||||
});
|
||||
const reView /* haha got it */ = new WebContentsView({
|
||||
webPreferences: {
|
||||
devTools: true,
|
||||
nodeIntegration: true
|
||||
}
|
||||
});
|
||||
win.contentView.addChildView(reView);
|
||||
|
||||
const commitProc = exec("git log origin/HEAD...HEAD --oneline --pretty=format:%an////////%h////////%H////////%s", {
|
||||
cwd: pluginDir
|
||||
});
|
||||
let rawOutput = "";
|
||||
commitProc.stdout?.on("data", d => {
|
||||
rawOutput += String(d);
|
||||
});
|
||||
commitProc.once("close", () => {
|
||||
win.loadURL(generateUpdatePluginContent({
|
||||
name: pluginMeta.name,
|
||||
description: pluginMeta.description,
|
||||
remote: pluginMeta.remote,
|
||||
commit: rawOutput.split("\n").map(line => line.split("////////")).map(([user, shortCommit, longCommit, message]) => `${user} (<a href="${pluginMeta.remote.replace("plugins.nin0.dev", "git.nin0.dev/userplugins")}/commit/${longCommit}" style="font-family: monospace;">${shortCommit}</a>) ~ ${message}`).join("\n")
|
||||
}));
|
||||
win.on("page-title-updated", async e => {
|
||||
if (win.webContents.getTitle().startsWith("openLink:")) {
|
||||
await shell.openExternal(win.webContents.getTitle().replace("openLink:", ""));
|
||||
}
|
||||
switch (win.webContents.getTitle() as "abortInstall" | "install") {
|
||||
case "abortInstall": {
|
||||
win.close();
|
||||
return reject("Rejected by user");
|
||||
}
|
||||
case "install": {
|
||||
win.close();
|
||||
try {
|
||||
const otherProc = exec("git rebase origin/HEAD", {
|
||||
cwd: pluginDir
|
||||
});
|
||||
let errored = "";
|
||||
otherProc.stderr?.on("data", d => { if (String(d).includes("Success")) return; errored += String(d); });
|
||||
otherProc.once("close", () => {
|
||||
if (errored) if (!errored.includes("Success")) return reject(`Failed to apply the update, chances are you have local changes that conflict with your remote. Git errors:\n\n${errored}`);
|
||||
build().then(() => resolve(JSON.stringify({
|
||||
name: pluginMeta.name,
|
||||
native: pluginMeta.usesNative
|
||||
})));
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
reject((e as Error).toString());
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
win.show();
|
||||
}
|
||||
doStuff();
|
||||
});
|
||||
}
|
||||
|
||||
export async function openGitPathModal(_: any) {
|
||||
const gitPathSet: string | undefined = NativeSettings.store.plugins.UserpluginInstaller?.gitPath;
|
||||
const win = new BrowserWindow({
|
||||
maximizable: false,
|
||||
minimizable: false,
|
||||
width: 560,
|
||||
height: 400,
|
||||
resizable: false,
|
||||
webPreferences: {
|
||||
devTools: true
|
||||
},
|
||||
title: "Set Git path",
|
||||
modal: true,
|
||||
parent: BrowserWindow.getAllWindows()[0],
|
||||
show: false,
|
||||
autoHideMenuBar: true
|
||||
});
|
||||
const reView = new WebContentsView({
|
||||
webPreferences: {
|
||||
devTools: true,
|
||||
nodeIntegration: true
|
||||
}
|
||||
});
|
||||
win.contentView.addChildView(reView);
|
||||
win.loadURL(`data:text/html;base64,${Buffer.from(setGitPathContent).toString("base64")}`);
|
||||
win.on("page-title-updated", async _ => {
|
||||
const t = win.webContents.getTitle();
|
||||
if (t === "abort") win.close();
|
||||
if (t.startsWith("ok")) {
|
||||
if (!NativeSettings.store.plugins.UserpluginInstaller) {
|
||||
NativeSettings.store.plugins.UserpluginInstaller = {
|
||||
gitPath: undefined
|
||||
};
|
||||
}
|
||||
if (t === "ok-") {
|
||||
NativeSettings.store.plugins.UserpluginInstaller.gitPath = undefined;
|
||||
} else {
|
||||
const gitPath2 = t.split("-").toSpliced(0, 1).join("-");
|
||||
NativeSettings.store.plugins.UserpluginInstaller.gitPath = gitPath2;
|
||||
}
|
||||
win.close();
|
||||
}
|
||||
if (t.startsWith("check")) {
|
||||
try {
|
||||
const gitProc = spawn(t === "check-" ? "git" : t.split("-").toSpliced(0, 1).join("-"), ["--version"]);
|
||||
let rawOutput = "";
|
||||
gitProc.stdout?.on("data", d => {
|
||||
rawOutput += String(d);
|
||||
});
|
||||
gitProc.on("error", e => {
|
||||
dialog.showMessageBox({
|
||||
title: "Error",
|
||||
message: "Git error",
|
||||
type: "error",
|
||||
detail: `${e}\n\nDouble-check the path you entered.`,
|
||||
buttons: ["OK"]
|
||||
});
|
||||
});
|
||||
gitProc.once("close", () => {
|
||||
if (gitProc.exitCode === 0) {
|
||||
dialog.showMessageBox({
|
||||
title: "Success",
|
||||
message: "Git works!",
|
||||
type: "info",
|
||||
detail: `Successfully called ${rawOutput.trim()}`,
|
||||
buttons: ["OK"]
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch(e) {
|
||||
dialog.showMessageBox({
|
||||
title: "Error",
|
||||
message: "Git error",
|
||||
type: "error",
|
||||
detail: `${e}\n\nDouble-check the path you entered.`,
|
||||
buttons: ["OK"]
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
win.show();
|
||||
if (gitPathSet) {
|
||||
win.webContents.executeJavaScript(`document.querySelector("input").value = ${JSON.stringify(gitPathSet)};`);
|
||||
}
|
||||
|
||||
console.log(YAML.load(`
|
||||
name: test
|
||||
items:
|
||||
- a
|
||||
- b
|
||||
`));
|
||||
}
|
||||
5175
src/equicordplugins/userpluginInstaller.dev/yaml.js
Normal file
5175
src/equicordplugins/userpluginInstaller.dev/yaml.js
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue