diff --git a/plugins/rose-plugs/src/index.ts b/plugins/rose-plugs/src/index.ts
index 9502067..7540806 100644
--- a/plugins/rose-plugs/src/index.ts
+++ b/plugins/rose-plugs/src/index.ts
@@ -1,22 +1,19 @@
import { logger } from "@vendetta";
import { getAssetIDByName } from "@vendetta/ui/assets";
import Settings from "./ui/Settings";
+import PluginsBrowser from "./ui/PluginsBrowser";
import patchRosiesPlugsSection, { SectionRow } from "./patches/settings";
import { discoverRosiesPlugins } from "./patches/discoverPlugins";
let unpatch: (() => void) | undefined;
-function StubPluginsScreen() {
- return null;
-}
-
function buildRows(): SectionRow[] {
const rows: SectionRow[] = [
{
key: "ROSES_PLUGS_BROWSER",
title: () => "Plugins",
icon: getAssetIDByName("SettingsIcon"),
- page: StubPluginsScreen,
+ page: PluginsBrowser,
},
];
diff --git a/plugins/rose-plugs/src/ui/PluginDetailSheet.tsx b/plugins/rose-plugs/src/ui/PluginDetailSheet.tsx
new file mode 100644
index 0000000..c1672f8
--- /dev/null
+++ b/plugins/rose-plugs/src/ui/PluginDetailSheet.tsx
@@ -0,0 +1,43 @@
+import { React, ReactNative } from "@vendetta/metro/common";
+import { plugins as installedPlugins, installPlugin } from "@vendetta/plugins";
+import { showToast } from "@vendetta/ui/toasts";
+import PrimaryButton from "@shared/ui/PrimaryButton";
+import { NexusPlugin } from "../lib/nexusApi";
+
+const { ScrollView, Text } = ReactNative;
+
+export default function PluginDetailSheet({ plugin }: { plugin: NexusPlugin }) {
+ const [installing, setInstalling] = React.useState(false);
+ const alreadyInstalled = plugin.installUrl in installedPlugins;
+
+ const onInstall = React.useCallback(() => {
+ setInstalling(true);
+ installPlugin(plugin.installUrl)
+ .then(() => {
+ setInstalling(false);
+ showToast(`${plugin.name} installed`);
+ })
+ .catch((e: any) => {
+ setInstalling(false);
+ showToast(`Install failed: ${e?.message ?? e}`);
+ });
+ }, [plugin.installUrl]);
+
+ return (
+
+ {plugin.name}
+ {plugin.category}
+ {plugin.tagline || plugin.description}
+ {plugin.status && plugin.status !== "default" && (
+
+ {plugin.status.toUpperCase()}
+
+ )}
+
+
+ );
+}
diff --git a/plugins/rose-plugs/src/ui/PluginsBrowser.tsx b/plugins/rose-plugs/src/ui/PluginsBrowser.tsx
new file mode 100644
index 0000000..349497c
--- /dev/null
+++ b/plugins/rose-plugs/src/ui/PluginsBrowser.tsx
@@ -0,0 +1,85 @@
+import { React, ReactNative } from "@vendetta/metro/common";
+import { plugins as installedPlugins } from "@vendetta/plugins";
+import { showCustomAlert } from "@vendetta/ui/alerts";
+import SettingsScaffold from "@shared/ui/SettingsScaffold";
+import NoteBox from "@shared/ui/NoteBox";
+import PrimaryButton from "@shared/ui/PrimaryButton";
+import { TableRow, TableRowGroup } from "@shared/ui/table";
+import { fetchNexusPlugins, NexusPlugin } from "../lib/nexusApi";
+import PluginDetailSheet from "./PluginDetailSheet";
+
+const { Text } = ReactNative;
+
+interface BrowserState {
+ loading: boolean;
+ error: string | null;
+ plugins: NexusPlugin[];
+}
+
+export default function PluginsBrowser() {
+ const [state, setState] = React.useState({ loading: true, error: null, plugins: [] });
+
+ const load = React.useCallback(() => {
+ setState((s) => ({ ...s, loading: true, error: null }));
+ fetchNexusPlugins()
+ .then((plugins) => setState({ loading: false, error: null, plugins }))
+ .catch((e) => setState({ loading: false, error: String(e?.message ?? e), plugins: [] }));
+ }, []);
+
+ React.useEffect(() => {
+ load();
+ }, [load]);
+
+ if (state.loading) {
+ return (
+
+ Loading Rosie's plugins…
+
+ );
+ }
+
+ if (state.error) {
+ return (
+
+ Couldn't load the plugin list: {state.error}
+
+
+ );
+ }
+
+ const byCategory = new Map();
+ for (const plugin of state.plugins) {
+ const list = byCategory.get(plugin.category) ?? [];
+ list.push(plugin);
+ byCategory.set(plugin.category, list);
+ }
+
+ return (
+
+ {[...byCategory.entries()].map(([category, plugins]) => (
+
+ {plugins.map((plugin) => {
+ const installed = plugin.installUrl in installedPlugins;
+ return (
+
+ {plugin.status.toUpperCase()}
+
+ ) : installed ? (
+ ✓
+ ) : undefined
+ }
+ onPress={() => showCustomAlert(PluginDetailSheet, { plugin })}
+ />
+ );
+ })}
+
+ ))}
+
+ );
+}