From 4f3d9902af372dd849f02a20dc1ae644d309fd73 Mon Sep 17 00:00:00 2001 From: Innei Date: Fri, 3 Jan 2025 00:43:14 +0800 Subject: [PATCH] fix(rn): discover form Signed-off-by: Innei --- .../src/components/ui/form/FormProvider.tsx | 19 ++ .../src/components/ui/form/TextField.tsx | 46 ++--- apps/mobile/src/icons/check_line.tsx | 24 +++ apps/mobile/src/modules/discover/search.tsx | 3 + .../src/screens/(modal)/rsshub-form.tsx | 189 +++++++++++------- apps/mobile/src/theme/colors.ts | 8 +- apps/mobile/src/theme/utils.ts | 8 +- packages/utils/src/color.ts | 34 ++++ 8 files changed, 228 insertions(+), 103 deletions(-) create mode 100644 apps/mobile/src/components/ui/form/FormProvider.tsx create mode 100644 apps/mobile/src/icons/check_line.tsx diff --git a/apps/mobile/src/components/ui/form/FormProvider.tsx b/apps/mobile/src/components/ui/form/FormProvider.tsx new file mode 100644 index 0000000000..ec8dfc1f97 --- /dev/null +++ b/apps/mobile/src/components/ui/form/FormProvider.tsx @@ -0,0 +1,19 @@ +import { createContext, useContext } from "react" +import type { FieldValues, UseFormReturn } from "react-hook-form" + +const FormContext = createContext | null>(null) + +export function FormProvider(props: { + form: UseFormReturn + children: React.ReactNode +}) { + return {props.children} +} + +export function useFormContext() { + const context = useContext(FormContext) + if (!context) { + throw new Error("useFormContext must be used within a FormProvider") + } + return context as UseFormReturn +} diff --git a/apps/mobile/src/components/ui/form/TextField.tsx b/apps/mobile/src/components/ui/form/TextField.tsx index 408e5b5dc9..f53005fe39 100644 --- a/apps/mobile/src/components/ui/form/TextField.tsx +++ b/apps/mobile/src/components/ui/form/TextField.tsx @@ -1,5 +1,5 @@ import { cn } from "@follow/utils/src/utils" -import type { FC } from "react" +import { forwardRef } from "react" import type { StyleProp, TextInputProps, ViewStyle } from "react-native" import { StyleSheet, TextInput, View } from "react-native" @@ -7,29 +7,27 @@ interface TextFieldProps { wrapperClassName?: string wrapperStyle?: StyleProp } -export const TextField: FC = ({ - className, - style, - wrapperClassName, - wrapperStyle, - ...rest -}) => { - return ( - - - - ) -} + +export const TextField = forwardRef( + ({ className, style, wrapperClassName, wrapperStyle, ...rest }, ref) => { + return ( + + + + ) + }, +) const styles = StyleSheet.create({ textField: { diff --git a/apps/mobile/src/icons/check_line.tsx b/apps/mobile/src/icons/check_line.tsx new file mode 100644 index 0000000000..49b194185b --- /dev/null +++ b/apps/mobile/src/icons/check_line.tsx @@ -0,0 +1,24 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface CheckLineIconProps { + width?: number + height?: number + color?: string +} + +export const CheckLineIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: CheckLineIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/modules/discover/search.tsx b/apps/mobile/src/modules/discover/search.tsx index 9e786b0608..2d539d44bc 100644 --- a/apps/mobile/src/modules/discover/search.tsx +++ b/apps/mobile/src/modules/discover/search.tsx @@ -38,6 +38,9 @@ export const SearchHeader = () => { } export const DiscoverHeader = () => { + return +} +const DiscoverHeaderImpl = () => { const frame = useSafeAreaFrame() const insets = useSafeAreaInsets() const headerHeight = getDefaultHeaderHeight(frame, false, insets.top) diff --git a/apps/mobile/src/screens/(modal)/rsshub-form.tsx b/apps/mobile/src/screens/(modal)/rsshub-form.tsx index ce072434e8..466f227438 100644 --- a/apps/mobile/src/screens/(modal)/rsshub-form.tsx +++ b/apps/mobile/src/screens/(modal)/rsshub-form.tsx @@ -1,21 +1,23 @@ import type { RSSHubParameter, RSSHubParameterObject, RSSHubRoute } from "@follow/models/src/rsshub" -import { parseFullPathParams, parseRegexpPathParams } from "@follow/utils" +import { parseFullPathParams, parseRegexpPathParams, withOpacity } from "@follow/utils" import { PortalProvider } from "@gorhom/portal" import { zodResolver } from "@hookform/resolvers/zod" import { router, Stack, useLocalSearchParams } from "expo-router" -import { useEffect, useMemo } from "react" -import type { UseFormReturn } from "react-hook-form" -import { useForm } from "react-hook-form" +import { memo, useEffect, useMemo } from "react" +import { Controller, useForm } from "react-hook-form" import { Linking, Text, TouchableOpacity, View } from "react-native" import { KeyboardAwareScrollView } from "react-native-keyboard-controller" import { z } from "zod" import { ModalHeaderCloseButton } from "@/src/components/common/ModalSharedComponents" +import { FormProvider, useFormContext } from "@/src/components/ui/form/FormProvider" import { FormLabel } from "@/src/components/ui/form/Label" import { Select } from "@/src/components/ui/form/Select" import { TextField } from "@/src/components/ui/form/TextField" import MarkdownWeb from "@/src/components/ui/typography/MarkdownWeb" +import { CheckLineIcon } from "@/src/icons/check_line" import { PreviewUrl } from "@/src/modules/rsshub/preview-url" +import { useColor } from "@/src/theme/colors" interface RsshubFormParams { route: RSSHubRoute @@ -99,72 +101,89 @@ function FormImpl({ route, routePrefix, name }: RsshubFormParams) { resolver: zodResolver(dynamicFormSchema), defaultValues: defaultValue, mode: "all", - }) as UseFormReturn + }) return ( - - - - - - {/* Form */} - - - {keys.map((keyItem) => { - const parameters = normalizeRSSHubParameters(route.parameters[keyItem.name]) - const formRegister = form.register(keyItem.name) - - return ( - - - {!parameters?.options && ( - { - formRegister.onChange({ - target: { value: text }, - }) - }} - /> - )} - - {!!parameters?.options && ( - { + formRegister.onChange({ + target: { + [keyItem.name]: value, + }, + }) + }} + /> + )} + + {!!parameters && ( + {parameters.description} + )} + + ) + })} + + + + + + + + + ) } @@ -191,3 +210,39 @@ const normalizeRSSHubParameters = (parameters: RSSHubParameter): RSSHubParameter ? { description: parameters, default: null } : parameters : null + +const ScreenOptions = memo(({ name, routeName }: { name: string; routeName: string }) => { + const form = useFormContext() + + return ( + ( + + + + ), + headerTitle: `${name} - ${routeName}`, + }} + /> + ) +}) + +const ModalHeaderSubmitButton = () => { + return +} +const ModalHeaderSubmitButtonImpl = () => { + const form = useFormContext() + const label = useColor("label") + const { isValid } = form.formState + const submit = form.handleSubmit((data) => { + void data + }) + + return ( + + + + ) +} diff --git a/apps/mobile/src/theme/colors.ts b/apps/mobile/src/theme/colors.ts index d17be298a8..4ef1c7bad7 100644 --- a/apps/mobile/src/theme/colors.ts +++ b/apps/mobile/src/theme/colors.ts @@ -1,3 +1,4 @@ +import { rgbStringToRgb } from "@follow/utils" import { useColorScheme, vars } from "nativewind" import { useMemo } from "react" @@ -132,11 +133,6 @@ export const darkVariants = { /// Utils -const toRgb = (hex: string) => { - const [r, g, b] = hex.split(" ").map((s) => Number.parseInt(s)) - return `rgb(${r} ${g} ${b})` -} - const mergedLightColors = { ...lightVariants, ...lightPalette, @@ -158,7 +154,7 @@ export const colorVariants = { export const useColor = (color: keyof typeof mergedLightColors) => { const { colorScheme } = useColorScheme() const colors = mergedColors[colorScheme || "light"] - return useMemo(() => toRgb(colors[color]), [color, colors]) + return useMemo(() => rgbStringToRgb(colors[color]), [color, colors]) } export const useColors = () => { diff --git a/apps/mobile/src/theme/utils.ts b/apps/mobile/src/theme/utils.ts index d69c76d7d0..5f45304337 100644 --- a/apps/mobile/src/theme/utils.ts +++ b/apps/mobile/src/theme/utils.ts @@ -1,3 +1,4 @@ +import { rgbStringToRgb } from "@follow/utils" import type { StyleProp, ViewStyle } from "react-native" import { Appearance, StyleSheet } from "react-native" @@ -16,10 +17,5 @@ export const getSystemBackgroundColor = () => { const colorScheme = Appearance.getColorScheme() || "light" const colors = colorScheme === "light" ? lightVariants : darkVariants - return toRgb(colors.systemBackground) -} - -const toRgb = (hex: string) => { - const [r, g, b] = hex.split(" ").map((s) => Number.parseInt(s)) - return `rgb(${r} ${g} ${b})` + return rgbStringToRgb(colors.systemBackground) } diff --git a/packages/utils/src/color.ts b/packages/utils/src/color.ts index ea85e0be85..40ad3bdd5d 100644 --- a/packages/utils/src/color.ts +++ b/packages/utils/src/color.ts @@ -127,3 +127,37 @@ export function getDominantColor(imageObject: HTMLImageElement) { return `#${((1 << 24) + (i[0] << 16) + (i[1] << 8) + i[2]).toString(16).slice(1)}` } + +export const isHexColor = (color: string) => { + return /^#[0-9a-f]{6}$/i.test(color) +} + +export const isRGBColor = (color: string) => { + return /^rgb\(\d{1,3},\s*\d{1,3},\s*\d{1,3}\)$/.test(color) +} +export const isRGBAColor = (color: string) => { + return /^rgba\(\d{1,3},\s*\d{1,3},\s*\d{1,3},\s*0?\.\d+\)$/.test(color) +} + +export const withOpacity = (color: string, opacity: number) => { + switch (true) { + case isHexColor(color): { + return `${color}${opacity.toString(16).slice(1)}` + } + case isRGBColor(color): { + const [r, g, b] = color.match(/\d+/g)!.map(Number) + return `rgba(${r}, ${g}, ${b}, ${opacity})` + } + case isRGBAColor(color): { + const [r, g, b] = color.match(/\d+/g)!.map(Number) + return `rgba(${r}, ${g}, ${b}, ${opacity})` + } + default: { + return color + } + } +} +export const rgbStringToRgb = (hex: string) => { + const [r, g, b] = hex.split(" ").map((s) => Number.parseInt(s)) + return `rgb(${r}, ${g}, ${b})` +}