[Plugin] Basic, non-functional implementation

This commit is contained in:
Beef 2023-01-03 00:18:19 +00:00
parent d0f4e87475
commit 7465e42354
6 changed files with 157 additions and 0 deletions

18
src/def.d.ts vendored
View file

@ -37,6 +37,22 @@ interface Assets {
[id: string]: Asset; [id: string]: Asset;
} }
interface PluginManifest {
name: string;
description: string;
icon?: string;
author: string;
}
interface Plugin {
id: string;
manifest: PluginManifest;
enabled: boolean;
js: string;
}
type Indexable<Type> = { [index: string]: Type }
interface VendettaObject { interface VendettaObject {
patcher: { patcher: {
after: typeof _spitroast.after; after: typeof _spitroast.after;
@ -91,9 +107,11 @@ interface VendettaObject {
} }
declare global { declare global {
type React = typeof _React;
interface Window { interface Window {
[key: PropertyKey]: any; [key: PropertyKey]: any;
modules: MetroModules; modules: MetroModules;
vendetta: VendettaObject; vendetta: VendettaObject;
React: typeof _React;
} }
} }

24
src/lib/plugins.ts Normal file
View file

@ -0,0 +1,24 @@
import { Indexable, PluginManifest, Plugin } from "@types";
export const plugins: Indexable<Plugin> = {};
export async function fetchPlugin(url: string) {
if (!url.endsWith("/")) url += "/";
if (plugins[url]) throw new Error(`That plugin is already installed!`);
let pluginManifest: PluginManifest;
try {
pluginManifest = await (await fetch(new URL("manifest.json", url), { cache: "no-store" })).json();
} catch {
throw new Error(`Failed to fetch manifest for ${url}`);
}
plugins[url] = {
id: url.split("://")[1],
manifest: pluginManifest,
enabled: true,
js: "",
};
}

View file

@ -0,0 +1,49 @@
import { ReactNative as RN, stylesheet } from "@metro/common";
import { Forms } from "@ui/components";
import { Plugin } from "@types";
import { getAssetIDByName } from "@/ui/assets";
const { FormRow, FormText, FormSwitch } = Forms;
const styles = stylesheet.createThemedStyleSheet({
card: {
backgroundColor: stylesheet.ThemeColorMap.BACKGROUND_SECONDARY,
borderRadius: 5,
margin: 10,
},
header: {
backgroundColor: stylesheet.ThemeColorMap.BACKGROUND_TERTIARY,
borderTopLeftRadius: 5,
borderTopRightRadius: 5,
}
})
interface PluginCardProps {
plugin: Plugin;
}
export default function PluginCard({ plugin }: PluginCardProps) {
const [enabled, setEnabled] = React.useState(plugin.enabled);
return (
<RN.View style={styles.card}>
<FormRow
style={styles.header}
label={`${plugin.manifest.name} by ${plugin.manifest.author}`}
leading={<FormRow.Icon source={getAssetIDByName(plugin.manifest.icon || "ic_application_command_24px")} />}
trailing={
<FormSwitch
value={plugin.enabled}
onValueChange={(v: boolean) => {
setEnabled(v);
plugin.enabled = enabled;
}}
/>
}
/>
<FormRow
label={plugin.manifest.description}
/>
</RN.View>
)
}

View file

@ -16,6 +16,12 @@ export default function SettingsSection({ navigation }: SettingsSectionProps) {
trailing={FormRow.Arrow} trailing={FormRow.Arrow}
onPress={() => navigation.push("VendettaSettings")} onPress={() => navigation.push("VendettaSettings")}
/> />
<FormRow
label="Plugins"
leading={() => <FormRow.Icon source={getAssetIDByName("debug")} />}
trailing={FormRow.Arrow}
onPress={() => navigation.push("VendettaPlugins")}
/>
<FormRow <FormRow
label="Asset Browser" label="Asset Browser"
leading={() => <FormRow.Icon source={getAssetIDByName("grid")} />} leading={() => <FormRow.Icon source={getAssetIDByName("grid")} />}

View file

@ -18,6 +18,10 @@ export default function initSettings() {
title: "Vendetta", title: "Vendetta",
render: General, render: General,
}, },
VendettaPlugins: {
title: "Plugins",
render: Plugins
},
VendettaAssetBrowser: { VendettaAssetBrowser: {
title: "Asset Browser", title: "Asset Browser",
render: AssetBrowser, render: AssetBrowser,

View file

@ -0,0 +1,56 @@
import { ReactNative as RN, stylesheet } from "@metro/common";
import { Forms } from "@ui/components";
import { showToast } from "@ui/toasts";
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"
}
})
export default function Plugins() {
const [pluginUrl, setPluginUrl] = React.useState("");
const [pluginList, setPluginList] = React.useState(plugins);
return (
<>
<FormInput
value={pluginUrl}
onChange={(v: string) => setPluginUrl(v)}
title="PLUGIN URL"
/>
<FormRow
label="Install plugin"
leading={() => <FormRow.Icon source={getAssetIDByName("add_white")} />}
trailing={FormRow.Arrow}
onPress={() => {
fetchPlugin(pluginUrl).then(() => {
setPluginUrl("");
setPluginList(plugins);
}).catch((e: Error) => {
showToast(e.message, getAssetIDByName("Small"));
});
}
}
/>
<RN.FlatList
data={Object.values(pluginList)}
renderItem={({ item }) => <PluginCard plugin={item} />}
keyExtractor={item => item.id}
/>
<RN.View style={styles.disclaimer}>
<FormText style={styles.disclaimerText}>Plugins are currently non-functional, but most of the infrastructure and UI is in place.</FormText>
</RN.View>
</>
)
}