diff --git a/src/lib/plugins.ts b/src/lib/plugins.ts index e80b4a9..6ab18c7 100644 --- a/src/lib/plugins.ts +++ b/src/lib/plugins.ts @@ -1,11 +1,19 @@ import { Indexable, PluginManifest, Plugin } from "@types"; +import logger from "./logger"; + +type EvaledPlugin = { + onLoad?(): void; + onUnload(): void; +}; export const plugins: Indexable = {}; +const loadedPlugins: Indexable = {}; export async function fetchPlugin(url: string) { if (!url.endsWith("/")) url += "/"; - if (plugins[url]) throw new Error(`That plugin is already installed!`); + const id = url.split("://")[1]; + if (typeof url !== "string" || url in plugins) throw new Error("Plugin ID invalid or taken"); let pluginManifest: PluginManifest; @@ -15,10 +23,72 @@ export async function fetchPlugin(url: string) { throw new Error(`Failed to fetch manifest for ${url}`); } - plugins[url] = { - id: url.split("://")[1], + let pluginJs: string; + + try { + pluginJs = await (await fetch(new URL("plugin.js", url), { cache: "no-store" })).text() + } catch { + throw new Error(`Failed to fetch JS for ${url}`) + } + + plugins[id] = { + id: id, manifest: pluginManifest, - enabled: true, - js: "", + enabled: false, + js: pluginJs, }; -} \ No newline at end of file +} + +// Bundlers don't like eval +const theSystemHasBeenDestroyed = eval; + +export function evalPlugin(plugin: Plugin) { + // TODO: Refactor to not depend on own window object + const vendettaForPlugins = Object.assign({}, window.vendetta); + const pluginString = `(vendetta)=>{return ${plugin.js}}\n//# sourceURL=${plugin.id}`; + + const ret = theSystemHasBeenDestroyed(pluginString)(vendettaForPlugins); + return typeof ret == "function" ? ret() : ret; +} + +export function startPlugin(id: string) { + const plugin = plugins[id]; + if (!plugin) throw new Error("Attempted to start non-existent plugin"); + + try { + const pluginRet: EvaledPlugin = evalPlugin(plugin); + loadedPlugins[id] = pluginRet; + pluginRet.onLoad?.(); + plugin.enabled = true; + } catch(e) { + logger.error(`Plugin ${plugin.id} errored whilst loading, and will be unloaded`, e); + + try { + loadedPlugins[plugin.id]?.onUnload?.(); + } catch(e2) { + logger.error(`Plugin ${plugin.id} errored whilst unloading`, e2); + } + + delete loadedPlugins[id]; + plugin.enabled = false; + } +} + +export function stopPlugin(id: string) { + const plugin = plugins[id]; + const pluginRet = loadedPlugins[id]; + if (!plugin) throw new Error("Attempted to stop non-existent plugin"); + if (!pluginRet) throw new Error("Attempted to stop a non-started plugin"); + + try { + loadedPlugins[plugin.id]?.onUnload?.(); + } catch(e) { + logger.error(`Plugin ${plugin.id} errored whilst unloading`, e); + } + + delete loadedPlugins[id]; + plugin.enabled = false; +} + +// TODO: When startAllPlugins exists, return this so cleanup in index.ts is easier +const stopAllPlugins = () => Object.keys(loadedPlugins).forEach(stopPlugin); \ No newline at end of file diff --git a/src/ui/settings/components/PluginCard.tsx b/src/ui/settings/components/PluginCard.tsx index 13df03a..9a03468 100644 --- a/src/ui/settings/components/PluginCard.tsx +++ b/src/ui/settings/components/PluginCard.tsx @@ -2,8 +2,9 @@ import { ReactNative as RN, stylesheet } from "@metro/common"; import { Forms } from "@ui/components"; import { Plugin } from "@types"; import { getAssetIDByName } from "@/ui/assets"; +import { startPlugin, stopPlugin } from "@/lib/plugins"; -const { FormRow, FormText, FormSwitch } = Forms; +const { FormRow, FormSwitch } = Forms; const styles = stylesheet.createThemedStyleSheet({ card: { @@ -35,8 +36,9 @@ export default function PluginCard({ plugin }: PluginCardProps) { { + alert(v); + if (v) startPlugin(plugin.id); else stopPlugin(plugin.id); setEnabled(v); - plugin.enabled = enabled; }} /> } diff --git a/src/ui/settings/pages/Plugins.tsx b/src/ui/settings/pages/Plugins.tsx index c866b1e..0cfb147 100644 --- a/src/ui/settings/pages/Plugins.tsx +++ b/src/ui/settings/pages/Plugins.tsx @@ -49,7 +49,7 @@ export default function Plugins() { keyExtractor={item => item.id} /> - Plugins are currently non-functional, but most of the infrastructure and UI is in place. + Plugins are currently non-permanent whilst I find a storage solution. )