[Plugin] Basic, non-functional implementation
This commit is contained in:
parent
d0f4e87475
commit
7465e42354
6 changed files with 157 additions and 0 deletions
18
src/def.d.ts
vendored
18
src/def.d.ts
vendored
|
@ -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
24
src/lib/plugins.ts
Normal 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: "",
|
||||||
|
};
|
||||||
|
}
|
49
src/ui/settings/components/PluginCard.tsx
Normal file
49
src/ui/settings/components/PluginCard.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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")} />}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
56
src/ui/settings/pages/Plugins.tsx
Normal file
56
src/ui/settings/pages/Plugins.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
Loading…
Reference in a new issue