import { React, ReactNative } from "@vendetta/metro/common"; import { plugins as installedPlugins, installPlugin, removePlugin } from "@vendetta/plugins"; import { showToast } from "@vendetta/ui/toasts"; 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"; const { Text, View, TouchableOpacity } = ReactNative; interface BrowserState { loading: boolean; error: string | null; plugins: NexusPlugin[]; } // installPlugin's promise doesn't always reliably settle - this timeout is a safety net so a hung // call can't leave a row stuck on "Installing..." forever. function withTimeout(promise: Promise, ms: number): Promise { return new Promise((resolve, reject) => { const timer = setTimeout(() => reject(new Error("Timed out")), ms); promise.then( (v) => { clearTimeout(timer); resolve(v); }, (e) => { clearTimeout(timer); reject(e); }, ); }); } export default function PluginsBrowser() { const [state, setState] = React.useState({ loading: true, error: null, plugins: [] }); const [busyId, setBusyId] = React.useState(null); 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]); const onInstall = React.useCallback((plugin: NexusPlugin) => { setBusyId(plugin.id); withTimeout(installPlugin(plugin.installUrl), 20000) .then(() => showToast(`${plugin.name} installed`)) .catch((e: any) => showToast(`Install failed: ${e?.message ?? e}`)) .finally(() => setBusyId(null)); }, []); const onUninstall = React.useCallback((plugin: NexusPlugin) => { setBusyId(plugin.id); try { removePlugin(plugin.installUrl); showToast(`${plugin.name} removed`); } catch (e: any) { showToast(`Remove failed: ${e?.message ?? e}`); } finally { setBusyId(null); } }, []); 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; const busy = busyId === plugin.id; return ( {plugin.status && plugin.status !== "default" && ( {plugin.status.toUpperCase()} )} (installed ? onUninstall(plugin) : onInstall(plugin))} style={{ backgroundColor: installed ? "rgba(35,165,90,0.2)" : "#5865F2", borderRadius: 6, paddingHorizontal: 10, paddingVertical: 5, opacity: busy ? 0.6 : 1, }} > {busy ? installed ? "Removing…" : "Installing…" : installed ? "✓ Installed" : "Install"} } /> ); })} ))} ); }