[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;
|
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 {
|
interface PluginAuthor {
|
||||||
name: string;
|
name: string;
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -313,6 +344,7 @@ interface VendettaObject {
|
||||||
constants: {
|
constants: {
|
||||||
DISCORD_SERVER: string;
|
DISCORD_SERVER: string;
|
||||||
GITHUB: string;
|
GITHUB: string;
|
||||||
|
HTTP_REGEX: RegExp;
|
||||||
};
|
};
|
||||||
utils: {
|
utils: {
|
||||||
copyText: (content: string) => void;
|
copyText: (content: string) => void;
|
||||||
|
@ -332,6 +364,7 @@ interface VendettaObject {
|
||||||
Forms: PropIntellisense<"Form" | "FormSection">;
|
Forms: PropIntellisense<"Form" | "FormSection">;
|
||||||
General: PropIntellisense<"Button" | "Text" | "View">;
|
General: PropIntellisense<"Button" | "Text" | "View">;
|
||||||
Search: _React.ComponentType;
|
Search: _React.ComponentType;
|
||||||
|
Alert: _React.ComponentType;
|
||||||
// Vendetta
|
// Vendetta
|
||||||
Summary: (props: SummaryProps) => JSX.Element;
|
Summary: (props: SummaryProps) => JSX.Element;
|
||||||
ErrorBoundary: (props: ErrorBoundaryProps) => JSX.Element;
|
ErrorBoundary: (props: ErrorBoundaryProps) => JSX.Element;
|
||||||
|
@ -339,6 +372,11 @@ interface VendettaObject {
|
||||||
toasts: {
|
toasts: {
|
||||||
showToast: (content: string, asset: number) => void;
|
showToast: (content: string, asset: number) => void;
|
||||||
};
|
};
|
||||||
|
alerts: {
|
||||||
|
showConfirmationAlert: (options: ConfirmationAlertOptions) => void;
|
||||||
|
showCustomAlert: (component: _React.ComponentType, props: any) => void;
|
||||||
|
showInputAlert: (options: InputAlertProps) => void;
|
||||||
|
};
|
||||||
assets: {
|
assets: {
|
||||||
all: Indexable<Asset>;
|
all: Indexable<Asset>;
|
||||||
find: (filter: (a: any) => void) => Asset | null | undefined;
|
find: (filter: (a: any) => void) => Asset | null | undefined;
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
export const DISCORD_SERVER = "n9QQ4XhhJP";
|
export const DISCORD_SERVER = "n9QQ4XhhJP";
|
||||||
export const GITHUB = "https://github.com/vendetta-mod";
|
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 common from "@metro/common";
|
||||||
import * as components from "@ui/components";
|
import * as components from "@ui/components";
|
||||||
import * as toasts from "@ui/toasts";
|
import * as toasts from "@ui/toasts";
|
||||||
|
import * as alerts from "@ui/alerts";
|
||||||
import * as assets from "@ui/assets";
|
import * as assets from "@ui/assets";
|
||||||
import * as color from "@ui/color";
|
import * as color from "@ui/color";
|
||||||
import * as utils from "@utils";
|
import * as utils from "@utils";
|
||||||
|
@ -30,6 +31,7 @@ export default async (unloads: any[]): Promise<VendettaObject> => ({
|
||||||
ui: {
|
ui: {
|
||||||
components,
|
components,
|
||||||
toasts,
|
toasts,
|
||||||
|
alerts,
|
||||||
assets,
|
assets,
|
||||||
...color,
|
...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 Forms = findByProps("Form", "FormSection");
|
||||||
export const General = findByProps("Button", "Text", "View");
|
export const General = findByProps("Button", "Text", "View");
|
||||||
export const Search = findByDisplayName("StaticSearchBarContainer");
|
export const Search = findByDisplayName("StaticSearchBarContainer");
|
||||||
|
export const Alert = findByProps("alertDarkStyles", "alertLightStyles").default;
|
||||||
|
|
||||||
// Vendetta
|
// Vendetta
|
||||||
export { default as Summary } from "@ui/components/Summary";
|
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 {
|
interface PluginCardProps {
|
||||||
plugin: Plugin;
|
plugin: Plugin;
|
||||||
|
index: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PluginCard({ plugin }: PluginCardProps) {
|
export default function PluginCard({ plugin, index }: PluginCardProps) {
|
||||||
const settings = getSettings(plugin.id);
|
const settings = getSettings(plugin.id);
|
||||||
const navigation = NavigationNative.useNavigation();
|
const navigation = NavigationNative.useNavigation();
|
||||||
const [removed, setRemoved] = React.useState(false);
|
const [removed, setRemoved] = React.useState(false);
|
||||||
|
@ -53,7 +54,7 @@ export default function PluginCard({ plugin }: PluginCardProps) {
|
||||||
if (removed) return null;
|
if (removed) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RN.View style={styles.card}>
|
<RN.View style={[styles.card, {marginTop: index === 0 ? 10 : 0}]}>
|
||||||
<FormRow
|
<FormRow
|
||||||
style={styles.header}
|
style={styles.header}
|
||||||
// TODO: Actually make use of user IDs
|
// 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 Plugins from "@ui/settings/pages/Plugins";
|
||||||
import Developer from "@ui/settings/pages/Developer";
|
import Developer from "@ui/settings/pages/Developer";
|
||||||
import AssetBrowser from "@ui/settings/pages/AssetBrowser";
|
import AssetBrowser from "@ui/settings/pages/AssetBrowser";
|
||||||
|
import InstallPluginButton from "@ui/settings/components/InstallPluginButton";
|
||||||
|
|
||||||
const screensModule = findByDisplayName("getScreens", false);
|
const screensModule = findByDisplayName("getScreens", false);
|
||||||
const settingsModule = findByDisplayName("UserSettingsOverviewWrapper", false);
|
const settingsModule = findByDisplayName("UserSettingsOverviewWrapper", false);
|
||||||
|
@ -25,6 +26,7 @@ export default function initSettings() {
|
||||||
VendettaPlugins: {
|
VendettaPlugins: {
|
||||||
title: "Plugins",
|
title: "Plugins",
|
||||||
render: Plugins,
|
render: Plugins,
|
||||||
|
headerRight: InstallPluginButton,
|
||||||
},
|
},
|
||||||
VendettaDeveloper: {
|
VendettaDeveloper: {
|
||||||
title: "Developer",
|
title: "Developer",
|
||||||
|
|
|
@ -1,44 +1,18 @@
|
||||||
import { ReactNative as RN } from "@metro/common";
|
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 { useProxy } from "@lib/storage";
|
||||||
import { plugins, installPlugin } from "@lib/plugins";
|
import { plugins } from "@lib/plugins";
|
||||||
import PluginCard from "@ui/settings/components/PluginCard";
|
import PluginCard from "@ui/settings/components/PluginCard";
|
||||||
import ErrorBoundary from "@ui/components/ErrorBoundary";
|
import ErrorBoundary from "@ui/components/ErrorBoundary";
|
||||||
|
|
||||||
const { FormInput, FormRow } = Forms;
|
|
||||||
|
|
||||||
export default function Plugins() {
|
export default function Plugins() {
|
||||||
useProxy(plugins);
|
useProxy(plugins);
|
||||||
const [pluginUrl, setPluginUrl] = React.useState("");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<RN.View style={{ flex: 1 }}>
|
<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
|
<RN.FlatList
|
||||||
style={{ marginTop: 10 }}
|
|
||||||
data={Object.values(plugins)}
|
data={Object.values(plugins)}
|
||||||
renderItem={({ item }) => <PluginCard plugin={item} />}
|
renderItem={({ item, index }) => <PluginCard plugin={item} index={index} />}
|
||||||
keyExtractor={item => item.id}
|
keyExtractor={item => item.id}
|
||||||
/>
|
/>
|
||||||
</RN.View>
|
</RN.View>
|
||||||
|
|
Loading…
Reference in a new issue