diff --git a/src/def.d.ts b/src/def.d.ts index 277ffdc..a6e7905 100644 --- a/src/def.d.ts +++ b/src/def.d.ts @@ -59,6 +59,7 @@ interface Plugin { id: string; manifest: PluginManifest; enabled: boolean; + update: boolean; js: string; } diff --git a/src/index.ts b/src/index.ts index 0c1562b..fb3a417 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,7 @@ import { all, find, getAssetByID, getAssetByName, getAssetIDByName } from "@ui/a import patchAssets from "@ui/assets"; import initSettings from "@ui/settings"; import { connectToDebugger, patchLogHook } from "@lib/debug"; +import { initPlugins } from "@lib/plugins"; console.log("Hello from Vendetta!"); @@ -48,6 +49,7 @@ async function init() { initSettings(); patchAssets(); patchLogHook(); + initPlugins(); } catch (e: Error | any) { erroredOnLoad = true; alert(`Vendetta failed to initialize... ${e.stack || e.toString()}`); diff --git a/src/lib/plugins.ts b/src/lib/plugins.ts index 6857f74..2988be5 100644 --- a/src/lib/plugins.ts +++ b/src/lib/plugins.ts @@ -1,5 +1,6 @@ import { Indexable, PluginManifest, Plugin } from "@types"; -import logger from "./logger"; +import { AsyncStorage } from "@metro/common"; +import logger from "@lib/logger"; type EvaledPlugin = { onLoad?(): void; @@ -7,21 +8,43 @@ type EvaledPlugin = { settings: JSX.Element; }; -export const plugins: Indexable = {}; +const proxyValidator = { + get(target: object, key: string | symbol): any { + const orig = Reflect.get(target, key); + + if (typeof orig === "object" && orig !== null) { + return new Proxy(orig, proxyValidator); + } else { + return orig; + } + }, + + set(target: object, key: string | symbol, value: any) { + Reflect.set(target, key, value); + AsyncStorage.setItem("VENDETTA_PLUGINS", JSON.stringify(plugins)); + return true; + }, + + deleteProperty(target: object, key: string | symbol) { + Reflect.deleteProperty(target, key); + AsyncStorage.setItem("VENDETTA_PLUGINS", JSON.stringify(plugins)); + return true; + } +} + +export const plugins: Indexable = new Proxy({}, proxyValidator); const loadedPlugins: Indexable = {}; -export async function fetchPlugin(url: string) { - if (!url.endsWith("/")) url += "/"; - - const id = url.split("://")[1]; - if (typeof url !== "string" || url in plugins) throw new Error("Plugin ID invalid or taken"); +export async function fetchPlugin(id: string) { + if (!id.endsWith("/")) id += "/"; + if (typeof id !== "string" || id in plugins) throw new Error("Plugin ID invalid or taken"); let pluginManifest: PluginManifest; try { - pluginManifest = await (await fetch(new URL("manifest.json", url), { cache: "no-store" })).json(); + pluginManifest = await (await fetch(new URL("manifest.json", id), { cache: "no-store" })).json(); } catch { - throw new Error(`Failed to fetch manifest for ${url}`); + throw new Error(`Failed to fetch manifest for ${id}`); } let pluginJs: string; @@ -29,17 +52,18 @@ export async function fetchPlugin(url: string) { // TODO: Remove duplicate error if possible try { // by polymanifest spec, plugins should always specify their main file, but just in case - pluginJs = await (await fetch(new URL(pluginManifest.main || "index.js", url), { cache: "no-store" })).text(); + pluginJs = await (await fetch(new URL(pluginManifest.main || "index.js", id), { cache: "no-store" })).text(); } catch { - throw new Error(`Failed to fetch JS for ${url}`); + throw new Error(`Failed to fetch JS for ${id}`); } - if (pluginJs.length === 0) throw new Error(`Failed to fetch JS for ${url}`); + if (pluginJs.length === 0) throw new Error(`Failed to fetch JS for ${id}`); plugins[id] = { id: id, manifest: pluginManifest, enabled: false, + update: true, js: pluginJs, }; } @@ -93,7 +117,26 @@ export function stopPlugin(id: string) { plugin.enabled = false; } +export function removePlugin(id: string) { + stopPlugin(id); + delete plugins[id]; +} + export const getSettings = (id: string) => loadedPlugins[id]?.settings; -// 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 +export const initPlugins = () => AsyncStorage.getItem("VENDETTA_PLUGINS").then(async function (v) { + if (!v) return; + const parsedPlugins: Indexable = JSON.parse(v); + + for (let p of Object.keys(parsedPlugins)) { + const plugin = parsedPlugins[p] + + if (parsedPlugins[p].update) { + await fetchPlugin(plugin.id); + } else { + plugins[p] = parsedPlugins[p]; + } + + if (parsedPlugins[p].enabled && plugins[p]) startPlugin(p); + } +}) diff --git a/src/ui/settings/components/PluginCard.tsx b/src/ui/settings/components/PluginCard.tsx index af3f25d..31d7bd6 100644 --- a/src/ui/settings/components/PluginCard.tsx +++ b/src/ui/settings/components/PluginCard.tsx @@ -2,7 +2,8 @@ import { ReactNative as RN, stylesheet, navigation } from "@metro/common"; import { Forms, General } from "@ui/components"; import { Plugin } from "@types"; import { getAssetIDByName } from "@ui/assets"; -import { getSettings, startPlugin, stopPlugin } from "@lib/plugins"; +import { getSettings, removePlugin, startPlugin, stopPlugin } from "@lib/plugins"; +import { showToast } from "@ui/toasts"; import PluginSettings from "@ui/settings/components/PluginSettings"; const { FormRow, FormSwitch } = Forms; @@ -27,6 +28,7 @@ const styles = stylesheet.createThemedStyleSheet({ icon: { width: 22, height: 22, + marginLeft: 5, tintColor: stylesheet.ThemeColorMap.INTERACTIVE_NORMAL, } }) @@ -37,8 +39,14 @@ interface PluginCardProps { export default function PluginCard({ plugin }: PluginCardProps) { const [enabled, setEnabled] = React.useState(plugin.enabled); + const [update, setUpdate] = React.useState(plugin.update); + const [removed, setRemoved] = React.useState(false); const Settings = getSettings(plugin.id); + // This is bad, but I don't think I have much choice - Beef + // Once the user re-renders the page, this is not taken into account anyway. + if (removed) return <>; + return ( + { + removePlugin(plugin.id); + setRemoved(true); + }} + > + + + { + plugin.update = !plugin.update; + setUpdate(plugin.update); + showToast(`${plugin.update ? "Enabled" : "Disabled"} updates for ${plugin.manifest.name}.`, getAssetIDByName("toast_image_saved")); + }} + > + + {Settings && { navigation.push(PluginSettings, { diff --git a/src/ui/settings/pages/Plugins.tsx b/src/ui/settings/pages/Plugins.tsx index 0cfb147..e8748a9 100644 --- a/src/ui/settings/pages/Plugins.tsx +++ b/src/ui/settings/pages/Plugins.tsx @@ -5,17 +5,7 @@ import { getAssetIDByName } from "@ui/assets"; import { fetchPlugin, plugins } from "@lib/plugins"; import PluginCard from "@ui/settings/components/PluginCard"; -const { FormInput, FormRow, FormText } = Forms; - -const styles = stylesheet.createThemedStyleSheet({ - disclaimer: { - backgroundColor: stylesheet.ThemeColorMap.BACKGROUND_SECONDARY, - padding: 10 - }, - disclaimerText: { - textAlign: "center" - } -}) +const { FormInput, FormRow } = Forms; export default function Plugins() { const [pluginUrl, setPluginUrl] = React.useState(""); @@ -48,9 +38,6 @@ export default function Plugins() { renderItem={({ item }) => } keyExtractor={item => item.id} /> - - Plugins are currently non-permanent whilst I find a storage solution. - ) } \ No newline at end of file