template_0205

This commit is contained in:
Sofio
2026-02-05 13:16:05 +08:00
commit d93e4d9c9f
197 changed files with 52810 additions and 0 deletions

View File

@@ -0,0 +1,228 @@
import {
createContext,
FC,
PropsWithChildren,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "react"
// eslint-disable-next-line no-restricted-imports
import { Animated, Modal, TextStyle, View, ViewStyle } from "react-native"
import { useSafeAreaInsets } from "react-native-safe-area-context"
import { Text } from "@/components/Text"
import { useAppTheme } from "@/theme/context"
import type { ThemedStyle } from "@/theme/types"
import { s } from "@/utils/responsive"
export type ToastType = "success" | "error" | "warning" | "info"
export interface ToastConfig {
message: string
type?: ToastType
duration?: number
}
export interface ToastContextType {
show: (config: ToastConfig) => void
success: (message: string, duration?: number) => void
error: (message: string, duration?: number) => void
warning: (message: string, duration?: number) => void
info: (message: string, duration?: number) => void
hide: () => void
}
// Default durations by type (in ms)
const DEFAULT_DURATIONS: Record<ToastType, number> = {
success: 3000,
error: 5000, // Errors show longer
warning: 4000,
info: 3000,
}
const ToastContext = createContext<ToastContextType | null>(null)
export const ToastProvider: FC<PropsWithChildren> = ({ children }) => {
const [visible, setVisible] = useState(false)
const [config, setConfig] = useState<ToastConfig>({ message: "" })
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const fadeAnim = useRef(new Animated.Value(0)).current
const insets = useSafeAreaInsets()
const {
themed,
theme: { colors },
} = useAppTheme()
// Clear timeout on unmount
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
}
}, [])
const hide = useCallback(() => {
// Clear any pending timeout
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
timeoutRef.current = null
}
// Fade out animation
Animated.timing(fadeAnim, {
toValue: 0,
duration: 200,
useNativeDriver: true,
}).start(() => {
setVisible(false)
})
}, [fadeAnim])
const show = useCallback(
(newConfig: ToastConfig) => {
// Clear any existing timeout to prevent premature hide
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
timeoutRef.current = null
}
setConfig(newConfig)
setVisible(true)
// Fade in animation
fadeAnim.setValue(0)
Animated.timing(fadeAnim, {
toValue: 1,
duration: 200,
useNativeDriver: true,
}).start()
// Auto hide after duration (use type-specific default if not provided)
const type = newConfig.type ?? "info"
const duration = newConfig.duration ?? DEFAULT_DURATIONS[type]
timeoutRef.current = setTimeout(() => {
hide()
}, duration)
},
[hide, fadeAnim],
)
const success = useCallback(
(message: string, duration?: number) => {
show({ message, type: "success", duration })
},
[show],
)
const error = useCallback(
(message: string, duration?: number) => {
show({ message, type: "error", duration })
},
[show],
)
const warning = useCallback(
(message: string, duration?: number) => {
show({ message, type: "warning", duration })
},
[show],
)
const info = useCallback(
(message: string, duration?: number) => {
show({ message, type: "info", duration })
},
[show],
)
const value = useMemo(
() => ({ show, success, error, warning, info, hide }),
[show, success, error, warning, info, hide],
)
const getBackgroundColor = (type: ToastType = "info") => {
switch (type) {
case "success":
return colors.palette.primary500
case "error":
return colors.error
case "warning":
return colors.palette.accent500
case "info":
default:
return colors.palette.secondary400
}
}
const getTextColor = (type: ToastType = "info") => {
switch (type) {
case "warning":
return colors.palette.neutral800
default:
return colors.palette.neutral100
}
}
return (
<ToastContext.Provider value={value}>
{children}
<Modal
visible={visible}
transparent
animationType="fade"
presentationStyle="overFullScreen"
statusBarTranslucent
onRequestClose={hide}
>
<Animated.View
pointerEvents="none"
style={[$overlay, { paddingTop: insets.top + 20 }, { opacity: fadeAnim }]}
>
<View
style={[themed($toastContainer), { backgroundColor: getBackgroundColor(config.type) }]}
>
<Text
text={config.message}
size="sm"
style={[themed($toastText), { color: getTextColor(config.type) }]}
/>
</View>
</Animated.View>
</Modal>
</ToastContext.Provider>
)
}
export const useToast = (): ToastContextType => {
const context = useContext(ToastContext)
if (!context) {
throw new Error("useToast must be used within a ToastProvider")
}
return context
}
const $overlay: ViewStyle = {
flex: 1,
alignItems: "center",
}
const $toastContainer: ThemedStyle<ViewStyle> = ({ spacing }) => ({
minWidth: s(120),
maxWidth: "80%",
paddingVertical: spacing.md,
paddingHorizontal: spacing.lg,
borderRadius: s(8),
shadowColor: "#000",
shadowOffset: { width: 0, height: s(2) },
shadowOpacity: 0.25,
shadowRadius: s(4),
elevation: 5,
})
const $toastText: ThemedStyle<TextStyle> = () => ({
textAlign: "center",
})