RosePlugs: inline install button on each row, plus a timeout safety net
This commit is contained in:
parent
986f9a6352
commit
a42d97decb
2 changed files with 68 additions and 18 deletions
|
|
@ -6,21 +6,28 @@ import { NexusPlugin } from "../lib/nexusApi";
|
||||||
|
|
||||||
const { ScrollView, Text } = ReactNative;
|
const { ScrollView, Text } = ReactNative;
|
||||||
|
|
||||||
|
// Confirmed live: this could get stuck on "Installing..." forever - the timeout below is a safety
|
||||||
|
// net so a hung installPlugin() call can't leave the button stuck indefinitely.
|
||||||
|
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
|
||||||
|
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 PluginDetailSheet({ plugin }: { plugin: NexusPlugin }) {
|
export default function PluginDetailSheet({ plugin }: { plugin: NexusPlugin }) {
|
||||||
const [installing, setInstalling] = React.useState(false);
|
const [installing, setInstalling] = React.useState(false);
|
||||||
const alreadyInstalled = plugin.installUrl in installedPlugins;
|
const alreadyInstalled = plugin.installUrl in installedPlugins;
|
||||||
|
|
||||||
const onInstall = React.useCallback(() => {
|
const onInstall = React.useCallback(() => {
|
||||||
setInstalling(true);
|
setInstalling(true);
|
||||||
installPlugin(plugin.installUrl)
|
withTimeout(installPlugin(plugin.installUrl), 20000)
|
||||||
.then(() => {
|
.then(() => showToast(`${plugin.name} installed`))
|
||||||
setInstalling(false);
|
.catch((e: any) => showToast(`Install failed: ${e?.message ?? e}`))
|
||||||
showToast(`${plugin.name} installed`);
|
.finally(() => setInstalling(false));
|
||||||
})
|
|
||||||
.catch((e: any) => {
|
|
||||||
setInstalling(false);
|
|
||||||
showToast(`Install failed: ${e?.message ?? e}`);
|
|
||||||
});
|
|
||||||
}, [plugin.installUrl]);
|
}, [plugin.installUrl]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { React, ReactNative } from "@vendetta/metro/common";
|
import { React, ReactNative } from "@vendetta/metro/common";
|
||||||
import { plugins as installedPlugins } from "@vendetta/plugins";
|
import { plugins as installedPlugins, installPlugin } from "@vendetta/plugins";
|
||||||
import { showCustomAlert } from "@vendetta/ui/alerts";
|
import { showCustomAlert } from "@vendetta/ui/alerts";
|
||||||
|
import { showToast } from "@vendetta/ui/toasts";
|
||||||
import SettingsScaffold from "@shared/ui/SettingsScaffold";
|
import SettingsScaffold from "@shared/ui/SettingsScaffold";
|
||||||
import NoteBox from "@shared/ui/NoteBox";
|
import NoteBox from "@shared/ui/NoteBox";
|
||||||
import PrimaryButton from "@shared/ui/PrimaryButton";
|
import PrimaryButton from "@shared/ui/PrimaryButton";
|
||||||
|
|
@ -8,7 +9,7 @@ import { TableRow, TableRowGroup } from "@shared/ui/table";
|
||||||
import { fetchNexusPlugins, NexusPlugin } from "../lib/nexusApi";
|
import { fetchNexusPlugins, NexusPlugin } from "../lib/nexusApi";
|
||||||
import PluginDetailSheet from "./PluginDetailSheet";
|
import PluginDetailSheet from "./PluginDetailSheet";
|
||||||
|
|
||||||
const { Text } = ReactNative;
|
const { Text, View, TouchableOpacity } = ReactNative;
|
||||||
|
|
||||||
interface BrowserState {
|
interface BrowserState {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
|
@ -16,8 +17,22 @@ interface BrowserState {
|
||||||
plugins: NexusPlugin[];
|
plugins: NexusPlugin[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Confirmed live: installing from the detail sheet could get stuck on "Installing..." forever -
|
||||||
|
// the promise from installPlugin wasn't reliably settling. This timeout is a safety net so a hung
|
||||||
|
// call can't leave a row stuck indefinitely, on top of moving the action to an inline button.
|
||||||
|
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
|
||||||
|
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() {
|
export default function PluginsBrowser() {
|
||||||
const [state, setState] = React.useState<BrowserState>({ loading: true, error: null, plugins: [] });
|
const [state, setState] = React.useState<BrowserState>({ loading: true, error: null, plugins: [] });
|
||||||
|
const [installingId, setInstallingId] = React.useState<string | null>(null);
|
||||||
|
|
||||||
const load = React.useCallback(() => {
|
const load = React.useCallback(() => {
|
||||||
setState((s) => ({ ...s, loading: true, error: null }));
|
setState((s) => ({ ...s, loading: true, error: null }));
|
||||||
|
|
@ -30,6 +45,14 @@ export default function PluginsBrowser() {
|
||||||
load();
|
load();
|
||||||
}, [load]);
|
}, [load]);
|
||||||
|
|
||||||
|
const onInstall = React.useCallback((plugin: NexusPlugin) => {
|
||||||
|
setInstallingId(plugin.id);
|
||||||
|
withTimeout(installPlugin(plugin.installUrl), 20000)
|
||||||
|
.then(() => showToast(`${plugin.name} installed`))
|
||||||
|
.catch((e: any) => showToast(`Install failed: ${e?.message ?? e}`))
|
||||||
|
.finally(() => setInstallingId(null));
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (state.loading) {
|
if (state.loading) {
|
||||||
return (
|
return (
|
||||||
<SettingsScaffold>
|
<SettingsScaffold>
|
||||||
|
|
@ -60,19 +83,39 @@ export default function PluginsBrowser() {
|
||||||
<TableRowGroup key={category} title={category}>
|
<TableRowGroup key={category} title={category}>
|
||||||
{plugins.map((plugin) => {
|
{plugins.map((plugin) => {
|
||||||
const installed = plugin.installUrl in installedPlugins;
|
const installed = plugin.installUrl in installedPlugins;
|
||||||
|
const installing = installingId === plugin.id;
|
||||||
return (
|
return (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={plugin.id}
|
key={plugin.id}
|
||||||
label={plugin.name}
|
label={plugin.name}
|
||||||
subLabel={installed ? "Installed" : plugin.tagline || plugin.description}
|
subLabel={installed ? "Installed" : plugin.tagline || plugin.description}
|
||||||
trailing={
|
trailing={
|
||||||
plugin.status && plugin.status !== "default" ? (
|
<View style={{ flexDirection: "row", alignItems: "center" }}>
|
||||||
<Text style={{ fontSize: 11, fontWeight: "700", opacity: 0.7 }}>
|
{plugin.status && plugin.status !== "default" && (
|
||||||
{plugin.status.toUpperCase()}
|
<Text style={{ fontSize: 11, fontWeight: "700", opacity: 0.7, marginRight: 8 }}>
|
||||||
</Text>
|
{plugin.status.toUpperCase()}
|
||||||
) : installed ? (
|
</Text>
|
||||||
<Text style={{ fontSize: 14, fontWeight: "700", color: "#23A55A" }}>✓</Text>
|
)}
|
||||||
) : undefined
|
{installed ? (
|
||||||
|
<Text style={{ fontSize: 14, fontWeight: "700", color: "#23A55A" }}>✓</Text>
|
||||||
|
) : (
|
||||||
|
<TouchableOpacity
|
||||||
|
disabled={installing}
|
||||||
|
onPress={() => onInstall(plugin)}
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#5865F2",
|
||||||
|
borderRadius: 6,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
paddingVertical: 5,
|
||||||
|
opacity: installing ? 0.6 : 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ color: "white", fontSize: 12, fontWeight: "600" }}>
|
||||||
|
{installing ? "Installing…" : "Install"}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
}
|
}
|
||||||
onPress={() => showCustomAlert(PluginDetailSheet, { plugin })}
|
onPress={() => showCustomAlert(PluginDetailSheet, { plugin })}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue