From 986f9a63521466ffe7018fe9b065b63b93d70e5f Mon Sep 17 00:00:00 2001 From: Ally Founder Date: Tue, 11 Aug 2026 18:36:41 -0400 Subject: [PATCH] RosePlugs: build the quick-install browser --- plugins/rose-plugs/src/index.ts | 7 +- .../rose-plugs/src/ui/PluginDetailSheet.tsx | 43 ++++++++++ plugins/rose-plugs/src/ui/PluginsBrowser.tsx | 85 +++++++++++++++++++ 3 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 plugins/rose-plugs/src/ui/PluginDetailSheet.tsx create mode 100644 plugins/rose-plugs/src/ui/PluginsBrowser.tsx 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 })} + /> + ); + })} + + ))} + + ); +}