diff --git a/src/core/ui/components/AddonCard.tsx b/src/core/ui/components/AddonCard.tsx new file mode 100644 index 0000000..c1d0651 --- /dev/null +++ b/src/core/ui/components/AddonCard.tsx @@ -0,0 +1,154 @@ +import { requireAssetIndex } from "@lib/api/assets"; +import { lazyDestructure } from "@lib/utils/lazy"; +import { Card, FormRadio, FormSwitch, IconButton, LegacyFormRow, Stack, Text } from "@metro/common/components"; +import { findByProps } from "@metro/utils"; +import { semanticColors } from "@ui/color"; +import { createStyles, TextStyleSheet } from "@ui/styles"; +import { TouchableOpacity, View } from "react-native"; + +const { hideActionSheet } = lazyDestructure(() => findByProps("openLazy", "hideActionSheet")); +const { showSimpleActionSheet } = lazyDestructure(() => findByProps("showSimpleActionSheet")); + +// TODO: These styles work weirdly. iOS has cramped text, Android with low DPI probably does too. Fix? +const useStyles = createStyles({ + card: { + backgroundColor: semanticColors?.CARD_SECONDARY_BG, + borderRadius: 12, + overflow: "hidden" + }, + header: { + padding: 0, + }, + headerLeading: { + flexDirection: "column", + justifyContent: "center", + scale: 1.2 + }, + headerTrailing: { + display: "flex", + flexDirection: "row", + gap: 15, + alignItems: "center" + }, + headerLabel: { + ...TextStyleSheet["heading-md/semibold"], + color: semanticColors.TEXT_NORMAL, + }, + headerSubtitle: { + ...TextStyleSheet["text-md/semibold"], + color: semanticColors.TEXT_MUTED, + }, + descriptionLabel: { + ...TextStyleSheet["text-md/semibold"], + color: semanticColors.TEXT_NORMAL, + }, + actions: { + flexDirection: "row-reverse", + alignItems: "center", + gap: 5 + }, + iconStyle: { + tintColor: semanticColors.LOGO_PRIMARY, + opacity: 0.2, + height: 64, + width: 64, + left: void 0, + right: "30%", + top: "-10%" + } +}); + +interface Action { + icon: string; + disabled?: boolean; + onPress: () => void; +} + +interface OverflowAction extends Action { + label: string; + isDestructive?: boolean; +} + +export interface CardWrapper { + item: T; + index: number; +} + +interface CardProps { + index?: number; + headerLabel: string; + headerSublabel?: string; + headerIcon?: string; + toggleType?: "switch" | "radio"; + toggleValue: () => boolean; + onToggleChange?: (v: boolean) => void; + descriptionLabel?: string; + actions?: Action[]; + overflowTitle?: string; + overflowActions?: OverflowAction[]; +} + +export default function AddonCard(props: CardProps) { + const styles = useStyles(); + + return ( + + + + + {props.headerLabel} + {props.headerSublabel && ( + {props.headerSublabel} + )} + + + + {props.overflowActions && + showSimpleActionSheet({ + key: "CardOverflow", + header: { + title: props.overflowTitle, + icon: props.headerIcon && , + onClose: () => hideActionSheet(), + }, + options: props.overflowActions?.map(i => ({ + ...i, + icon: requireAssetIndex(i.icon) + })), + })} + size="sm" + variant="secondary" + icon={requireAssetIndex("CircleInformationIcon-primary")} + />} + {props.actions?.map(({ icon, onPress, disabled }) => ( + + ))} + + {props.toggleType && (props.toggleType === "switch" ? + + : + { + props.onToggleChange?.(!props.toggleValue()); + }}> + + + )} + + + {props.descriptionLabel && + {props.descriptionLabel} + } + + + ); +} diff --git a/src/core/ui/components/AddonPage.tsx b/src/core/ui/components/AddonPage.tsx index d27a6b0..b0c9fa6 100644 --- a/src/core/ui/components/AddonPage.tsx +++ b/src/core/ui/components/AddonPage.tsx @@ -1,13 +1,11 @@ import { Strings } from "@core/i18n"; -import { CardWrapper } from "@core/ui/components/Card"; +import { CardWrapper } from "@core/ui/components/AddonCard"; import { requireAssetIndex } from "@lib/api/assets"; import { useProxy } from "@lib/api/storage"; import { settings } from "@lib/settings"; import { HTTP_REGEX_MULTI } from "@lib/utils/constants"; -import { lazyDestructure } from "@lib/utils/lazy"; import { clipboard } from "@metro/common"; -import { HelpMessage } from "@metro/common/components"; -import { findByProps, findByPropsProxy } from "@metro/utils"; +import { FloatingActionButton, HelpMessage } from "@metro/common/components"; import { showInputAlert } from "@ui/alerts"; import { ErrorBoundary, Search } from "@ui/components"; import fuzzysort from "fuzzysort"; @@ -18,14 +16,12 @@ export const RemoveModeContext = createContext(false); interface AddonPageProps { title: string; - floatingButtonText: string; fetchFunction: (url: string) => Promise; items: Record; safeModeMessage: string; safeModeExtras?: JSX.Element | JSX.Element[]; card: React.ComponentType>; isRemoveMode?: boolean; - headerComponent?: JSX.Element; onFABPress?: () => void; } @@ -44,57 +40,42 @@ function getItemsByQuery["items"][string]>(ite }).map(r => r.obj); } -const reanimated = findByPropsProxy("useSharedValue"); -const { FloatingActionButton } = lazyDestructure(() => findByProps("FloatingActionButton")); - -export default function AddonPage({ floatingButtonText, fetchFunction, items, safeModeMessage, safeModeExtras, card: CardComponent, isRemoveMode, headerComponent, onFABPress }: AddonPageProps) { - useProxy(items); +export default function AddonPage({ card: CardComponent, ...props }: AddonPageProps) { + useProxy(props.items); useProxy(settings); - const collapseText = reanimated.useSharedValue(0); - const yOffset = React.useRef(0); const [search, setSearch] = React.useState(""); return ( {/* TODO: Implement better searching than just by ID */} + ListHeaderComponent={ {settings.safeMode?.enabled && - {safeModeMessage} - {safeModeExtras} + {props.safeModeMessage} + {props.safeModeExtras} } setSearch(v.toLowerCase())} placeholder={Strings.SEARCH} /> - {headerComponent} - } - onScroll={e => { - if (e.nativeEvent.contentOffset.y <= 0) return; - collapseText.value = Number(e.nativeEvent.contentOffset.y > yOffset.current); - yOffset.current = e.nativeEvent.contentOffset.y; - }} + } style={{ paddingHorizontal: 10, paddingTop: 10 }} - contentContainerStyle={{ paddingBottom: 90, paddingHorizontal: 5 }} - data={getItemsByQuery(Object.values(items).filter(i => typeof i === "object"), search)} - renderItem={({ item, index }) => + contentContainerStyle={{ paddingBottom: 90, paddingHorizontal: 5, gap: 12 }} + data={getItemsByQuery(Object.values(props.items).filter(i => typeof i === "object"), search)} + renderItem={({ item, index }) => } /> { + onPress={props.onFABPress ?? (() => { // from ./InstallButton.tsx clipboard.getString().then(content => showInputAlert({ - title: floatingButtonText, initialValue: content.match(HTTP_REGEX_MULTI)?.[0] ?? "", placeholder: Strings.URL_PLACEHOLDER, - onConfirm: (input: string) => fetchFunction(input), + onConfirm: (input: string) => props.fetchFunction(input), confirmText: Strings.INSTALL, cancelText: Strings.CANCEL, }) diff --git a/src/core/ui/components/Card.tsx b/src/core/ui/components/Card.tsx deleted file mode 100644 index 86bc54e..0000000 --- a/src/core/ui/components/Card.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import { requireAssetIndex } from "@lib/api/assets"; -import { lazyDestructure } from "@lib/utils/lazy"; -import { ReactNative as RN } from "@metro/common"; -import { FormCheckbox, FormSwitch, IconButton, LegacyFormRow } from "@metro/common/components"; -import { findByProps } from "@metro/utils"; -import { semanticColors } from "@ui/color"; -import { createStyles, TextStyleSheet } from "@ui/styles"; -import { ImageBackground, View } from "react-native"; - -const { hideActionSheet } = lazyDestructure(() => findByProps("openLazy", "hideActionSheet")); -const { showSimpleActionSheet } = lazyDestructure(() => findByProps("showSimpleActionSheet")); - -// TODO: These styles work weirdly. iOS has cramped text, Android with low DPI probably does too. Fix? -const useStyles = createStyles({ - card: { - backgroundColor: semanticColors?.CARD_SECONDARY_BG, - borderRadius: 12, - overflow: "hidden" - }, - header: { - padding: 0, - }, - headerLeading: { - flexDirection: "column", - justifyContent: "center", - scale: 1.2 - }, - headerTrailing: { - display: "flex", - flexDirection: "row", - gap: 15, - alignItems: "center" - }, - headerLabel: { - ...TextStyleSheet["heading-md/semibold"], - color: semanticColors.TEXT_NORMAL, - }, - headerSubtitle: { - ...TextStyleSheet["text-md/semibold"], - color: semanticColors.TEXT_MUTED, - }, - descriptionLabel: { - ...TextStyleSheet["text-md/semibold"], - color: semanticColors.TEXT_NORMAL, - }, - actions: { - flexDirection: "row-reverse", - alignItems: "center", - gap: 5 - }, - iconStyle: { - tintColor: semanticColors.LOGO_PRIMARY, - opacity: 0.2, - height: 64, - width: 64, - left: void 0, - right: "30%", - top: "-10%" - } -}); - -interface Action { - icon: string; - disabled?: boolean; - onPress: () => void; -} - -interface OverflowAction extends Action { - label: string; - isDestructive?: boolean; -} - -export interface CardWrapper { - item: T; - index: number; -} - -interface CardProps { - index?: number; - headerLabel: string; - headerSublabel?: string; - headerIcon?: string; - toggleType?: "switch" | "radio"; - toggleValue?: boolean; - onToggleChange?: (v: boolean) => void; - descriptionLabel?: string; - actions?: Action[]; - overflowTitle?: string; - overflowActions?: OverflowAction[]; -} - -export default function Card(props: CardProps) { - const styles = useStyles(); - - return ( - - - - {props.headerLabel} - {props.headerSublabel && ( - {props.headerSublabel} - )} - - } - trailing={ - - - {props.overflowActions && - showSimpleActionSheet({ - key: "CardOverflow", - header: { - title: props.overflowTitle, - icon: props.headerIcon && , - onClose: () => hideActionSheet(), - }, - options: props.overflowActions?.map(i => ({ ...i, icon: requireAssetIndex(i.icon) })), - })} - size="sm" - variant="secondary" - icon={requireAssetIndex("CircleInformationIcon-primary")} - />} - {props.actions?.map(({ icon, onPress, disabled }) => ( - - ))} - - {props.toggleType && (props.toggleType === "switch" ? - - : - { - props.onToggleChange?.(!props.toggleValue); - }}> - - - )} - - } - /> - - {props.descriptionLabel} - - } - /> - - - ); -} diff --git a/src/core/ui/components/PluginCard.tsx b/src/core/ui/components/PluginCard.tsx deleted file mode 100644 index a3b8972..0000000 --- a/src/core/ui/components/PluginCard.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { formatString, Strings } from "@core/i18n"; -import Card, { CardWrapper } from "@core/ui/components/Card"; -import { requireAssetIndex } from "@lib/api/assets"; -import { purgeStorage, useProxy } from "@lib/api/storage"; -import { BunnyPlugin, fetchPlugin, getSettings, removePlugin, startPlugin, stopPlugin } from "@lib/managers/plugins"; -import { ButtonColors } from "@lib/utils/types"; -import { clipboard, NavigationNative } from "@metro/common"; -import { showConfirmationAlert } from "@ui/alerts"; -import { showToast } from "@ui/toasts"; - -async function stopThenStart(plugin: BunnyPlugin, callback: Function) { - if (plugin.enabled) stopPlugin(plugin.id, false); - callback(); - if (plugin.enabled) await startPlugin(plugin.id); -} - -export default function PluginCard({ item: plugin, index }: CardWrapper) { - useProxy(plugin); - - const settings = getSettings(plugin.id); - const navigation = NavigationNative.useNavigation(); - const [removed, setRemoved] = React.useState(false); - - // This is needed because of Reactâ„¢ - if (removed) return null; - - return ( - i.name).join(", ")}`} - headerIcon={plugin.manifest.vendetta?.icon} - toggleType="switch" - toggleValue={plugin.enabled} - onToggleChange={(v: boolean) => { - try { - if (v) startPlugin(plugin.id); else stopPlugin(plugin.id); - } catch (e) { - showToast((e as Error).message, requireAssetIndex("Small")); - } - }} - descriptionLabel={plugin.manifest.description} - overflowTitle={plugin.manifest.name} - actions={[{ - icon: "WrenchIcon", - disabled: !settings, - onPress: () => navigation.push("VendettaCustomPage", { - title: plugin.manifest.name, - render: settings, - }) - }]} - overflowActions={[ - ...(settings ? [{ - label: Strings.OVERFLOW_PLUGIN_SETTINGS, - icon: "settings", - onPress: () => navigation.push("VendettaCustomPage", { - title: plugin.manifest.name, - render: settings, - }) - }] : []), - { - icon: "ic_sync_24px", - label: Strings.REFETCH, - onPress: async () => { - stopThenStart(plugin, () => { - fetchPlugin(plugin.id).then(async () => { - showToast(Strings.PLUGIN_REFETCH_SUCCESSFUL, requireAssetIndex("toast_image_saved")); - }).catch(() => { - showToast(Strings.PLUGIN_REFETCH_FAILED, requireAssetIndex("Small")); - }); - }); - }, - }, - { - icon: "copy", - label: Strings.COPY_URL, - onPress: () => { - clipboard.setString(plugin.id); - showToast.showCopyToClipboard(); - } - }, - { - icon: "ic_download_24px", - label: plugin.update ? Strings.DISABLE_UPDATES : Strings.ENABLE_UPDATES, - onPress: () => { - plugin.update = !plugin.update; - showToast(formatString("TOASTS_PLUGIN_UPDATE", { update: plugin.update, name: plugin.manifest.name }), requireAssetIndex("toast_image_saved")); - } - }, - { - icon: "ic_duplicate", - label: Strings.CLEAR_DATA, - isDestructive: true, - onPress: () => showConfirmationAlert({ - title: Strings.HOLD_UP, - content: formatString("ARE_YOU_SURE_TO_CLEAR_DATA", { name: plugin.manifest.name }), - confirmText: Strings.CLEAR, - cancelText: Strings.CANCEL, - confirmColor: ButtonColors.RED, - onConfirm: () => { - stopThenStart(plugin, () => { - try { - purgeStorage(plugin.id); - showToast(formatString("CLEAR_DATA_SUCCESSFUL", { name: plugin.manifest.name }), requireAssetIndex("trash")); - } catch { - showToast(formatString("CLEAR_DATA_FAILED", { name: plugin.manifest.name }), requireAssetIndex("Small")); - } - }); - } - }), - }, - { - icon: "ic_message_delete", - label: Strings.DELETE, - isDestructive: true, - onPress: () => showConfirmationAlert({ - title: Strings.HOLD_UP, - content: formatString("ARE_YOU_SURE_TO_DELETE_PLUGIN", { name: plugin.manifest.name }), - confirmText: Strings.DELETE, - cancelText: Strings.CANCEL, - confirmColor: ButtonColors.RED, - onConfirm: () => { - try { - removePlugin(plugin.id); - setRemoved(true); - } catch (e) { - showToast((e as Error).message, requireAssetIndex("Small")); - } - } - }), - } - ]} - /> - ); -} diff --git a/src/core/ui/settings/pages/AssetBrowser.tsx b/src/core/ui/settings/pages/Developer/AssetBrowser.tsx similarity index 92% rename from src/core/ui/settings/pages/AssetBrowser.tsx rename to src/core/ui/settings/pages/Developer/AssetBrowser.tsx index 05cab50..0b32115 100644 --- a/src/core/ui/settings/pages/AssetBrowser.tsx +++ b/src/core/ui/settings/pages/Developer/AssetBrowser.tsx @@ -1,4 +1,4 @@ -import AssetDisplay from "@core/ui/components/AssetDisplay"; +import AssetDisplay from "@core/ui/settings/pages/Developer/AssetDisplay"; import { assetsMap } from "@lib/api/assets"; import { LegacyFormDivider } from "@metro/common/components"; import { ErrorBoundary, Search } from "@ui/components"; diff --git a/src/core/ui/components/AssetDisplay.tsx b/src/core/ui/settings/pages/Developer/AssetDisplay.tsx similarity index 100% rename from src/core/ui/components/AssetDisplay.tsx rename to src/core/ui/settings/pages/Developer/AssetDisplay.tsx diff --git a/src/core/ui/settings/pages/Developer.tsx b/src/core/ui/settings/pages/Developer/index.tsx similarity index 99% rename from src/core/ui/settings/pages/Developer.tsx rename to src/core/ui/settings/pages/Developer/index.tsx index ac9c76e..85a52b0 100644 --- a/src/core/ui/settings/pages/Developer.tsx +++ b/src/core/ui/settings/pages/Developer/index.tsx @@ -1,6 +1,6 @@ import { Strings } from "@core/i18n"; import { CheckState, useFileExists } from "@core/ui/settings/hooks/useFS"; -import AssetBrowser from "@core/ui/settings/pages/AssetBrowser"; +import AssetBrowser from "@core/ui/settings/pages/Developer/AssetBrowser"; import { requireAssetIndex } from "@lib/api/assets"; import { getReactDevToolsProp, getReactDevToolsVersion, isLoaderConfigSupported, isReactDevToolsPreloaded, isVendettaLoader } from "@lib/api/native/loader"; import { useProxy } from "@lib/api/storage"; diff --git a/src/core/ui/components/FontCard.tsx b/src/core/ui/settings/pages/Fonts/FontCard.tsx similarity index 92% rename from src/core/ui/components/FontCard.tsx rename to src/core/ui/settings/pages/Fonts/FontCard.tsx index 44d2050..e1b4f3e 100644 --- a/src/core/ui/components/FontCard.tsx +++ b/src/core/ui/settings/pages/Fonts/FontCard.tsx @@ -1,5 +1,6 @@ import { Strings } from "@core/i18n"; -import { CardWrapper } from "@core/ui/components/Card"; +import { CardWrapper } from "@core/ui/components/AddonCard"; +import { RemoveModeContext } from "@core/ui/components/AddonPage"; import { requireAssetIndex } from "@lib/api/assets"; import { useProxy } from "@lib/api/storage"; import { FontDefinition, fonts, removeFont, selectFont } from "@lib/managers/fonts"; @@ -8,8 +9,6 @@ import { showToast } from "@ui/toasts"; import { useContext } from "react"; import { Pressable, View } from "react-native"; -import { RemoveModeContext } from "./AddonPage"; - export default function FontCard({ item: font, index }: CardWrapper) { useProxy(fonts); diff --git a/src/core/ui/settings/pages/FontImporter.tsx b/src/core/ui/settings/pages/Fonts/FontImporter.tsx similarity index 99% rename from src/core/ui/settings/pages/FontImporter.tsx rename to src/core/ui/settings/pages/Fonts/FontImporter.tsx index db9be7d..64d592c 100644 --- a/src/core/ui/settings/pages/FontImporter.tsx +++ b/src/core/ui/settings/pages/Fonts/FontImporter.tsx @@ -47,7 +47,7 @@ function RevengeFontsExtractor({ fonts, setName }: { placeholder={fontName || "Whitney"} onChange={setFontName} errorMessage={error} - status={error ? "error" : void 0} + state={error ? "error" : void 0} /> {formatString("THEME_EXTRACTOR_DESC", { @@ -96,7 +96,7 @@ function JsonFontImporter({ fonts, setName }: { placeholder={"https://link.to/font/pack.json"} onChange={setFontLink} errorMessage={error} - status={error ? "error" : void 0} + state={error ? "error" : void 0} />