diff --git a/src/def.d.ts b/src/def.d.ts index 180c15e..64a53d9 100644 --- a/src/def.d.ts +++ b/src/def.d.ts @@ -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; + cancelText: string | undefined; + placeholder: string | undefined; + initialValue: string | undefined; +} + interface PluginAuthor { name: string; id: string; @@ -230,13 +261,13 @@ type Indexable = { [index: string]: Type } type EmitterEvent = "SET" | "GET" | "DEL"; interface EmitterListenerData { - path: string[]; - value?: any; + path: string[]; + value?: any; } type EmitterListener = ( - event: EmitterEvent, - data: EmitterListenerData | any + event: EmitterEvent, + data: EmitterListenerData | any ) => any; type EmitterListeners = Indexable>; @@ -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; find: (filter: (a: any) => void) => Asset | null | undefined; diff --git a/src/lib/constants.ts b/src/lib/constants.ts index f807041..a35589e 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1,2 +1,3 @@ export const DISCORD_SERVER = "n9QQ4XhhJP"; -export const GITHUB = "https://github.com/vendetta-mod"; \ No newline at end of file +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()@:%_\+.~#?&\/=]*)$/; \ No newline at end of file diff --git a/src/lib/windowObject.ts b/src/lib/windowObject.ts index 1facb80..3608d95 100644 --- a/src/lib/windowObject.ts +++ b/src/lib/windowObject.ts @@ -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 => ({ ui: { components, toasts, + alerts, assets, ...color, }, diff --git a/src/ui/alerts.ts b/src/ui/alerts.ts new file mode 100644 index 0000000..4f1da59 --- /dev/null +++ b/src/ui/alerts.ts @@ -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 { + 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); +}; \ No newline at end of file diff --git a/src/ui/components/InputAlert.tsx b/src/ui/components/InputAlert.tsx new file mode 100644 index 0000000..2a07614 --- /dev/null +++ b/src/ui/components/InputAlert.tsx @@ -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 ( + Alerts.close()} + > + { + setValue(v); + if (error) setError(""); + }} + returnKeyType="done" + onSubmitEditing={onConfirmWrapper} + error={error} + autoFocus={true} + showBorder={true} + style={{ paddingVertical: 5, alignSelf: "stretch", paddingHorizontal: 0 }} + /> + + ); +}; \ No newline at end of file diff --git a/src/ui/components/index.ts b/src/ui/components/index.ts index c1a028a..c1ee49a 100644 --- a/src/ui/components/index.ts +++ b/src/ui/components/index.ts @@ -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"; diff --git a/src/ui/settings/components/InstallPluginButton.tsx b/src/ui/settings/components/InstallPluginButton.tsx new file mode 100644 index 0000000..1660092 --- /dev/null +++ b/src/ui/settings/components/InstallPluginButton.tsx @@ -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 ( + + 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" + })) + }> + + + ); +}; \ No newline at end of file diff --git a/src/ui/settings/components/PluginCard.tsx b/src/ui/settings/components/PluginCard.tsx index 812a6eb..c2caa06 100644 --- a/src/ui/settings/components/PluginCard.tsx +++ b/src/ui/settings/components/PluginCard.tsx @@ -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 ( - + - setPluginUrl(v)} - placeholder="https://example.com/" - title="PLUGIN URL" - /> - } - onPress={() => { - installPlugin(pluginUrl).then(() => { - setPluginUrl(""); - }).catch((e: Error) => { - showToast(e.message, getAssetIDByName("Small")); - }); - } - } - /> } + renderItem={({ item, index }) => } keyExtractor={item => item.id} />