Skip to content

Commit

Permalink
fix(rn): discover form
Browse files Browse the repository at this point in the history
Signed-off-by: Innei <tukon479@gmail.com>
  • Loading branch information
Innei committed Jan 2, 2025
1 parent 1439d87 commit 4f3d990
Show file tree
Hide file tree
Showing 8 changed files with 228 additions and 103 deletions.
19 changes: 19 additions & 0 deletions apps/mobile/src/components/ui/form/FormProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { createContext, useContext } from "react"
import type { FieldValues, UseFormReturn } from "react-hook-form"

const FormContext = createContext<UseFormReturn<any> | null>(null)

export function FormProvider<T extends FieldValues>(props: {
form: UseFormReturn<T>
children: React.ReactNode
}) {
return <FormContext.Provider value={props.form}>{props.children}</FormContext.Provider>
}

export function useFormContext<T extends FieldValues>() {

Check warning on line 13 in apps/mobile/src/components/ui/form/FormProvider.tsx

View workflow job for this annotation

GitHub Actions / Format, Lint and Typecheck (lts/*)

Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components
const context = useContext(FormContext)
if (!context) {
throw new Error("useFormContext must be used within a FormProvider")
}
return context as UseFormReturn<T>
}
46 changes: 22 additions & 24 deletions apps/mobile/src/components/ui/form/TextField.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,33 @@
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"

interface TextFieldProps {
wrapperClassName?: string
wrapperStyle?: StyleProp<ViewStyle>
}
export const TextField: FC<TextInputProps & TextFieldProps> = ({
className,
style,
wrapperClassName,
wrapperStyle,
...rest
}) => {
return (
<View
className={cn(
"bg-system-fill/40 relative h-10 flex-row items-center rounded-lg px-4",
wrapperClassName,
)}
style={wrapperStyle}
>
<TextInput
className={cn("text-text placeholder:text-placeholder-text w-full flex-1", className)}
style={StyleSheet.flatten([styles.textField, style])}
{...rest}
/>
</View>
)
}

export const TextField = forwardRef<TextInput, TextInputProps & TextFieldProps>(
({ className, style, wrapperClassName, wrapperStyle, ...rest }, ref) => {
return (
<View
className={cn(
"bg-system-fill/40 relative h-10 flex-row items-center rounded-lg px-4",
wrapperClassName,
)}
style={wrapperStyle}
>
<TextInput
ref={ref}
className={cn("text-text placeholder:text-placeholder-text w-full flex-1", className)}
style={StyleSheet.flatten([styles.textField, style])}
{...rest}
/>
</View>
)
},
)

const styles = StyleSheet.create({
textField: {
Expand Down
24 changes: 24 additions & 0 deletions apps/mobile/src/icons/check_line.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path d="M24 0v24H0V0h24ZM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035c-.01-.004-.019-.001-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427c-.002-.01-.009-.017-.017-.018Zm.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093c.012.004.023 0 .029-.008l.004-.014l-.034-.614c-.003-.012-.01-.02-.02-.022Zm-.715.002a.023.023 0 0 0-.027.006l-.006.014l-.034.614c0 .012.007.02.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01l-.184-.092Z" />
<Path
fill={color}
d="M21.192 5.465a1 1 0 0 1 0 1.414L9.95 18.122a1.1 1.1 0 0 1-1.556 0l-5.586-5.586a1 1 0 1 1 1.415-1.415l4.95 4.95L19.777 5.465a1 1 0 0 1 1.414 0Z"
/>
</Svg>
)
}
3 changes: 3 additions & 0 deletions apps/mobile/src/modules/discover/search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ export const SearchHeader = () => {
}

export const DiscoverHeader = () => {
return <DiscoverHeaderImpl />
}
const DiscoverHeaderImpl = () => {
const frame = useSafeAreaFrame()
const insets = useSafeAreaInsets()
const headerHeight = getDefaultHeaderHeight(frame, false, insets.top)
Expand Down
189 changes: 122 additions & 67 deletions apps/mobile/src/screens/(modal)/rsshub-form.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -99,72 +101,89 @@ function FormImpl({ route, routePrefix, name }: RsshubFormParams) {
resolver: zodResolver(dynamicFormSchema),
defaultValues: defaultValue,
mode: "all",
}) as UseFormReturn<any>
})

return (
<PortalProvider>
<KeyboardAwareScrollView className="bg-system-grouped-background">
<Stack.Screen
options={{ headerLeft: ModalHeaderCloseButton, headerTitle: `${name} - ${routeName}` }}
/>

<PreviewUrl
className="m-2 mb-6"
watch={form.watch}
path={route.path}
routePrefix={routePrefix}
/>
{/* Form */}

<View className="bg-system-grouped-background-2 mx-2 gap-4 rounded-lg px-3 py-6">
{keys.map((keyItem) => {
const parameters = normalizeRSSHubParameters(route.parameters[keyItem.name])
const formRegister = form.register(keyItem.name)

return (
<View key={keyItem.name}>
<FormLabel className="pl-1" label={keyItem.name} optional={keyItem.optional} />
{!parameters?.options && (
<TextField
wrapperClassName="mt-2"
placeholder={formPlaceholder[keyItem.name]}
value={form.getValues(keyItem.name)}
onChangeText={(text) => {
formRegister.onChange({
target: { value: text },
})
}}
/>
)}

{!!parameters?.options && (
<Select
wrapperClassName="mt-2"
options={parameters.options}
value={form.getValues(keyItem.name)}
onValueChange={(value) => {
formRegister.onChange({ target: { value } })
}}
/>
)}

{!!parameters && (
<Text className="text-text/80 ml-2 mt-2 text-xs">{parameters.description}</Text>
)}
</View>
)
})}
</View>
<Maintainers maintainers={route.maintainers} />

<View className="mx-4 mt-4">
<MarkdownWeb
value={route.description.replaceAll("::: ", ":::")}
dom={{ matchContents: true, scrollEnabled: false }}
<FormProvider form={form}>
<ScreenOptions name={name} routeName={routeName} />

<PortalProvider>
<KeyboardAwareScrollView className="bg-system-grouped-background">
<PreviewUrl
className="m-2 mb-6"
watch={form.watch}
path={route.path}
routePrefix={routePrefix}
/>
</View>
</KeyboardAwareScrollView>
</PortalProvider>
{/* Form */}

<View className="bg-system-grouped-background-2 mx-2 gap-4 rounded-lg px-3 py-6">
{keys.map((keyItem) => {
const parameters = normalizeRSSHubParameters(route.parameters[keyItem.name])
const formRegister = form.register(keyItem.name)

return (
<View key={keyItem.name}>
<FormLabel className="pl-1" label={keyItem.name} optional={keyItem.optional} />
{!parameters?.options && (
<Controller
name={keyItem.name}
control={form.control}
rules={{
required: !keyItem.optional,
// validate: (value) => {
// return dynamicFormSchema.safeParse({
// [keyItem.name]: value,
// }).success
// },
}}
render={({ field: { onChange, onBlur, ref, value } }) => (
<TextField
wrapperClassName="mt-2"
placeholder={formPlaceholder[keyItem.name]}
onBlur={onBlur}
onChangeText={onChange}
defaultValue={defaultValue[keyItem.name] ?? ""}
value={value ?? ""}
ref={ref}
/>
)}
/>
)}

{!!parameters?.options && (
<Select
wrapperClassName="mt-2"
options={parameters.options}
value={form.getValues(keyItem.name)}
onValueChange={(value) => {
formRegister.onChange({
target: {
[keyItem.name]: value,
},
})
}}
/>
)}

{!!parameters && (
<Text className="text-text/80 ml-2 mt-2 text-xs">{parameters.description}</Text>
)}
</View>
)
})}
</View>
<Maintainers maintainers={route.maintainers} />

<View className="mx-4 mt-4">
<MarkdownWeb
value={route.description.replaceAll("::: ", ":::")}
dom={{ matchContents: true, scrollEnabled: false }}
/>
</View>
</KeyboardAwareScrollView>
</PortalProvider>
</FormProvider>
)
}

Expand All @@ -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 (
<Stack.Screen
options={{
headerLeft: ModalHeaderCloseButton,
headerRight: () => (
<FormProvider form={form}>
<ModalHeaderSubmitButton />
</FormProvider>
),
headerTitle: `${name} - ${routeName}`,
}}
/>
)
})

const ModalHeaderSubmitButton = () => {
return <ModalHeaderSubmitButtonImpl />
}
const ModalHeaderSubmitButtonImpl = () => {
const form = useFormContext()
const label = useColor("label")
const { isValid } = form.formState
const submit = form.handleSubmit((data) => {
void data
})

return (
<TouchableOpacity onPress={submit} disabled={!isValid}>
<CheckLineIcon color={isValid ? label : withOpacity(label, 0.5)} />
</TouchableOpacity>
)
}
8 changes: 2 additions & 6 deletions apps/mobile/src/theme/colors.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { rgbStringToRgb } from "@follow/utils"
import { useColorScheme, vars } from "nativewind"
import { useMemo } from "react"

Expand Down Expand Up @@ -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,
Expand All @@ -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 = () => {
Expand Down
8 changes: 2 additions & 6 deletions apps/mobile/src/theme/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { rgbStringToRgb } from "@follow/utils"
import type { StyleProp, ViewStyle } from "react-native"
import { Appearance, StyleSheet } from "react-native"

Expand All @@ -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)
}
Loading

0 comments on commit 4f3d990

Please sign in to comment.