[UI] Alerts API (#21)
* [UI] Initial Alerts API * [UI > Alerts] showCustomAlert and showInputAlert * [Constants] Add HTTP_REGEX * [UI > Plugins] Use InputAlert for installing plugins * [UI > Plugins/PluginCard] Pass plugin index to PluginCard to add top margin * [UI > Alerts] Fix indentation
This commit is contained in:
parent
c3f7d60d85
commit
9fb99ced74
10 changed files with 175 additions and 35 deletions
38
src/def.d.ts
vendored
38
src/def.d.ts
vendored
|
@ -46,6 +46,37 @@ interface Asset {
|
|||
id: number;
|
||||
}
|
||||
|
||||
declare enum ButtonColors {
|
||||
BRAND = "brand",
|
||||
RED = "red",
|
||||
GREEN = "green",
|
||||
PRIMARY = "primary",
|
||||
TRANSPARENT = "transparent",
|
||||
GREY = "grey",
|
||||
LIGHTGREY = "lightgrey",
|
||||
WHITE = "white",
|
||||
LINK = "link"
|
||||
}
|
||||
|
||||
interface ConfirmationAlertOptions {
|
||||
title: string | undefined;
|
||||
content: string | JSX.Element | JSX.Element[];
|
||||
confirmText: string | undefined;
|
||||
confirmColor: ButtonColors | undefined;
|
||||
onConfirm: () => void;
|
||||
cancelText: string | undefined;
|
||||
}
|
||||
|
||||
interface InputAlertProps {
|
||||
title: string | undefined;
|
||||
confirmText: string | undefined;
|
||||
confirmColor: ButtonColors | undefined;
|
||||
onConfirm: (input: string) => void | Promise<void>;
|
||||
cancelText: string | undefined;
|
||||
placeholder: string | undefined;
|
||||
initialValue: string | undefined;
|
||||
}
|
||||
|
||||
interface PluginAuthor {
|
||||
name: string;
|
||||
id: string;
|
||||
|
@ -313,6 +344,7 @@ interface VendettaObject {
|
|||
constants: {
|
||||
DISCORD_SERVER: string;
|
||||
GITHUB: string;
|
||||
HTTP_REGEX: RegExp;
|
||||
};
|
||||
utils: {
|
||||
copyText: (content: string) => void;
|
||||
|
@ -332,6 +364,7 @@ interface VendettaObject {
|
|||
Forms: PropIntellisense<"Form" | "FormSection">;
|
||||
General: PropIntellisense<"Button" | "Text" | "View">;
|
||||
Search: _React.ComponentType;
|
||||
Alert: _React.ComponentType;
|
||||
// Vendetta
|
||||
Summary: (props: SummaryProps) => JSX.Element;
|
||||
ErrorBoundary: (props: ErrorBoundaryProps) => JSX.Element;
|
||||
|
@ -339,6 +372,11 @@ interface VendettaObject {
|
|||
toasts: {
|
||||
showToast: (content: string, asset: number) => void;
|
||||
};
|
||||
alerts: {
|
||||
showConfirmationAlert: (options: ConfirmationAlertOptions) => void;
|
||||
showCustomAlert: (component: _React.ComponentType, props: any) => void;
|
||||
showInputAlert: (options: InputAlertProps) => void;
|
||||
};
|
||||
assets: {
|
||||
all: Indexable<Asset>;
|
||||
find: (filter: (a: any) => void) => Asset | null | undefined;
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
export const DISCORD_SERVER = "n9QQ4XhhJP";
|
||||
export const GITHUB = "https://github.com/vendetta-mod";
|
||||
export const HTTP_REGEX = /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)$/;
|
|
@ -11,6 +11,7 @@ import * as metro from "@metro/filters";
|
|||
import * as common from "@metro/common";
|
||||
import * as components from "@ui/components";
|
||||
import * as toasts from "@ui/toasts";
|
||||
import * as alerts from "@ui/alerts";
|
||||
import * as assets from "@ui/assets";
|
||||
import * as color from "@ui/color";
|
||||
import * as utils from "@utils";
|
||||
|
@ -30,6 +31,7 @@ export default async (unloads: any[]): Promise<VendettaObject> => ({
|
|||
ui: {
|
||||
components,
|
||||
toasts,
|
||||
alerts,
|
||||
assets,
|
||||
...color,
|
||||
},
|
||||
|
|
37
src/ui/alerts.ts
Normal file
37
src/ui/alerts.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { ConfirmationAlertOptions, InputAlertProps } from "@types";
|
||||
import { findByProps } from "@metro/filters";
|
||||
import InputAlert from "@ui/components/InputAlert";
|
||||
|
||||
const Alerts = findByProps("openLazy", "close");
|
||||
|
||||
interface InternalConfirmationAlertOptions extends Omit<ConfirmationAlertOptions, 'content'> {
|
||||
content: string | JSX.Element | JSX.Element[] | undefined;
|
||||
body: string | undefined;
|
||||
children: JSX.Element | JSX.Element[];
|
||||
};
|
||||
|
||||
export function showConfirmationAlert(options: ConfirmationAlertOptions) {
|
||||
const internalOptions = options as InternalConfirmationAlertOptions;
|
||||
|
||||
if (typeof options.content === "string") {
|
||||
internalOptions.body = options.content;
|
||||
} else {
|
||||
internalOptions.children = options.content;
|
||||
};
|
||||
|
||||
delete internalOptions.content;
|
||||
|
||||
return Alerts.show(internalOptions);
|
||||
};
|
||||
|
||||
export function showCustomAlert(component: React.ComponentType, props: any) {
|
||||
Alerts.openLazy({
|
||||
importer: async function () {
|
||||
return () => React.createElement(component, props);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export function showInputAlert(options: InputAlertProps) {
|
||||
showCustomAlert(InputAlert as React.ComponentType, options);
|
||||
};
|
49
src/ui/components/InputAlert.tsx
Normal file
49
src/ui/components/InputAlert.tsx
Normal file
|
@ -0,0 +1,49 @@
|
|||
import { findByProps } from "@metro/filters";
|
||||
import { Forms, Alert } from "@ui/components";
|
||||
import { InputAlertProps } from "@types";
|
||||
|
||||
const { FormInput } = Forms;
|
||||
|
||||
const Alerts = findByProps("openLazy", "close");
|
||||
|
||||
export default function InputAlert({ title, confirmText, confirmColor, onConfirm, cancelText, placeholder, initialValue = "" }: InputAlertProps) {
|
||||
const [value, setValue] = React.useState(initialValue);
|
||||
const [error, setError] = React.useState("");
|
||||
|
||||
function onConfirmWrapper() {
|
||||
const asyncOnConfirm = Promise.resolve(onConfirm(value))
|
||||
|
||||
asyncOnConfirm.then(() => {
|
||||
Alerts.close();
|
||||
}).catch((e: Error) => {
|
||||
setError(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Alert
|
||||
title={title}
|
||||
confirmText={confirmText}
|
||||
confirmColor={confirmColor}
|
||||
isConfirmButtonDisabled={error.length !== 0}
|
||||
onConfirm={onConfirmWrapper}
|
||||
cancelText={cancelText}
|
||||
onCancel={() => Alerts.close()}
|
||||
>
|
||||
<FormInput
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChangeText={(v: string) => {
|
||||
setValue(v);
|
||||
if (error) setError("");
|
||||
}}
|
||||
returnKeyType="done"
|
||||
onSubmitEditing={onConfirmWrapper}
|
||||
error={error}
|
||||
autoFocus={true}
|
||||
showBorder={true}
|
||||
style={{ paddingVertical: 5, alignSelf: "stretch", paddingHorizontal: 0 }}
|
||||
/>
|
||||
</Alert>
|
||||
);
|
||||
};
|
|
@ -4,6 +4,7 @@ import { findByDisplayName, findByProps } from "@metro/filters";
|
|||
export const Forms = findByProps("Form", "FormSection");
|
||||
export const General = findByProps("Button", "Text", "View");
|
||||
export const Search = findByDisplayName("StaticSearchBarContainer");
|
||||
export const Alert = findByProps("alertDarkStyles", "alertLightStyles").default;
|
||||
|
||||
// Vendetta
|
||||
export { default as Summary } from "@ui/components/Summary";
|
||||
|
|
35
src/ui/settings/components/InstallPluginButton.tsx
Normal file
35
src/ui/settings/components/InstallPluginButton.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { HTTP_REGEX } from "@/lib/constants";
|
||||
import { semanticColors } from "@ui/color";
|
||||
import { installPlugin } from "@lib/plugins";
|
||||
import { clipboard, stylesheet } from "@metro/common";
|
||||
import { showInputAlert } from "@ui/alerts";
|
||||
import { getAssetIDByName } from "@ui/assets";
|
||||
import { General } from "@ui/components";
|
||||
|
||||
const { TouchableOpacity, Image } = General;
|
||||
|
||||
const styles = stylesheet.createThemedStyleSheet({
|
||||
icon: {
|
||||
marginRight: 10,
|
||||
tintColor: semanticColors.HEADER_PRIMARY,
|
||||
}
|
||||
});
|
||||
|
||||
export default function InstallPluginButton() {
|
||||
return (
|
||||
<TouchableOpacity onPress={() =>
|
||||
clipboard.getString().then((content) =>
|
||||
showInputAlert({
|
||||
title: "Install Plugin",
|
||||
initialValue: HTTP_REGEX.test(content) ? content : "",
|
||||
placeholder: "https://example.com/",
|
||||
onConfirm: installPlugin,
|
||||
confirmText: "Install",
|
||||
confirmColor: undefined,
|
||||
cancelText: "Cancel"
|
||||
}))
|
||||
}>
|
||||
<Image style={styles.icon} source={getAssetIDByName("ic_add_24px")} />
|
||||
</TouchableOpacity >
|
||||
);
|
||||
};
|
|
@ -42,9 +42,10 @@ const styles = stylesheet.createThemedStyleSheet({
|
|||
|
||||
interface PluginCardProps {
|
||||
plugin: Plugin;
|
||||
index: number;
|
||||
}
|
||||
|
||||
export default function PluginCard({ plugin }: PluginCardProps) {
|
||||
export default function PluginCard({ plugin, index }: PluginCardProps) {
|
||||
const settings = getSettings(plugin.id);
|
||||
const navigation = NavigationNative.useNavigation();
|
||||
const [removed, setRemoved] = React.useState(false);
|
||||
|
@ -53,7 +54,7 @@ export default function PluginCard({ plugin }: PluginCardProps) {
|
|||
if (removed) return null;
|
||||
|
||||
return (
|
||||
<RN.View style={styles.card}>
|
||||
<RN.View style={[styles.card, {marginTop: index === 0 ? 10 : 0}]}>
|
||||
<FormRow
|
||||
style={styles.header}
|
||||
// TODO: Actually make use of user IDs
|
||||
|
|
|
@ -8,6 +8,7 @@ import General from "@ui/settings/pages/General";
|
|||
import Plugins from "@ui/settings/pages/Plugins";
|
||||
import Developer from "@ui/settings/pages/Developer";
|
||||
import AssetBrowser from "@ui/settings/pages/AssetBrowser";
|
||||
import InstallPluginButton from "@ui/settings/components/InstallPluginButton";
|
||||
|
||||
const screensModule = findByDisplayName("getScreens", false);
|
||||
const settingsModule = findByDisplayName("UserSettingsOverviewWrapper", false);
|
||||
|
@ -25,6 +26,7 @@ export default function initSettings() {
|
|||
VendettaPlugins: {
|
||||
title: "Plugins",
|
||||
render: Plugins,
|
||||
headerRight: InstallPluginButton,
|
||||
},
|
||||
VendettaDeveloper: {
|
||||
title: "Developer",
|
||||
|
|
|
@ -1,44 +1,18 @@
|
|||
import { ReactNative as RN } from "@metro/common";
|
||||
import { Forms } from "@ui/components";
|
||||
import { showToast } from "@ui/toasts";
|
||||
import { getAssetIDByName } from "@ui/assets";
|
||||
import { useProxy } from "@lib/storage";
|
||||
import { plugins, installPlugin } from "@lib/plugins";
|
||||
import { plugins } from "@lib/plugins";
|
||||
import PluginCard from "@ui/settings/components/PluginCard";
|
||||
import ErrorBoundary from "@ui/components/ErrorBoundary";
|
||||
|
||||
const { FormInput, FormRow } = Forms;
|
||||
|
||||
export default function Plugins() {
|
||||
useProxy(plugins);
|
||||
const [pluginUrl, setPluginUrl] = React.useState("");
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<RN.View style={{ flex: 1 }}>
|
||||
<FormInput
|
||||
value={pluginUrl}
|
||||
onChange={(v: string) => setPluginUrl(v)}
|
||||
placeholder="https://example.com/"
|
||||
title="PLUGIN URL"
|
||||
/>
|
||||
<FormRow
|
||||
label="Install plugin"
|
||||
// I checked, this icon exists on a fresh Discord install. Please, stop disappearing.
|
||||
leading={<FormRow.Icon source={getAssetIDByName("ic_add_24px")} />}
|
||||
onPress={() => {
|
||||
installPlugin(pluginUrl).then(() => {
|
||||
setPluginUrl("");
|
||||
}).catch((e: Error) => {
|
||||
showToast(e.message, getAssetIDByName("Small"));
|
||||
});
|
||||
}
|
||||
}
|
||||
/>
|
||||
<RN.FlatList
|
||||
style={{ marginTop: 10 }}
|
||||
data={Object.values(plugins)}
|
||||
renderItem={({ item }) => <PluginCard plugin={item} />}
|
||||
renderItem={({ item, index }) => <PluginCard plugin={item} index={index} />}
|
||||
keyExtractor={item => item.id}
|
||||
/>
|
||||
</RN.View>
|
||||
|
|
Loading…
Reference in a new issue