Merge branch 'kkdev'
This commit is contained in:
commit
d50e656c9f
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"chatgpt.openOnStartup": true,
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ import { Image } from 'expo-image'
|
|||
import { APP_VERSION } from '@/app.constants'
|
||||
|
||||
import * as Sentry from '@sentry/react-native'
|
||||
import { KeyboardAwareScrollView } from 'react-native-keyboard-controller'
|
||||
|
||||
export default function TabTwoScreen() {
|
||||
const {
|
||||
|
|
@ -197,10 +198,7 @@ export default function TabTwoScreen() {
|
|||
}
|
||||
|
||||
return (
|
||||
<ParallaxScrollView
|
||||
headerBackgroundColor={{ light: '#D0D0D0', dark: '#353636' }}
|
||||
headerImage={<IconSymbol size={310} color="#808080" name="paperplane.fill" style={styles.headerImage} />}
|
||||
>
|
||||
<KeyboardAwareScrollView bottomOffset={100} contentContainerStyle={{ paddingHorizontal: 20, paddingTop: 40, backgroundColor: 'white' }}>
|
||||
<ThemedView style={styles.titleContainer}>
|
||||
<ThemedText type="title">BLE Explorer</ThemedText>
|
||||
<ThemedText>插件当前版本号: {APP_VERSION}</ThemedText>
|
||||
|
|
@ -308,7 +306,7 @@ export default function TabTwoScreen() {
|
|||
onChangeText={setUserId}
|
||||
placeholder="输入用户ID"
|
||||
placeholderTextColor="#999"
|
||||
editable={isConnected}
|
||||
// editable={isConnected}
|
||||
/>
|
||||
</ThemedView>
|
||||
<ThemedView style={styles.buttonRow}>
|
||||
|
|
@ -325,7 +323,7 @@ export default function TabTwoScreen() {
|
|||
<ThemedText>Write UUID: {BLE_UUIDS.WRITE_CHARACTERISTIC}</ThemedText>
|
||||
<ThemedText>Read UUID: {BLE_UUIDS.READ_CHARACTERISTIC}</ThemedText>
|
||||
</ThemedView>
|
||||
</ParallaxScrollView>
|
||||
</KeyboardAwareScrollView>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { Dimensions, FlatList, TextInput, RefreshControl, ActivityIndicator } fr
|
|||
import { LinearGradient } from 'expo-linear-gradient'
|
||||
import Animated, { Easing, useAnimatedStyle, useSharedValue, withRepeat, withTiming } from 'react-native-reanimated'
|
||||
|
||||
import { router } from 'expo-router'
|
||||
import { router, useRouter } from 'expo-router'
|
||||
import { IOS_UNIVERSAL_LINK } from '@/app.constants'
|
||||
import { FlashList } from '@shopify/flash-list'
|
||||
import { screenHeight, screenWidth } from '@/utils'
|
||||
|
|
@ -51,11 +51,36 @@ const Banner = memo<BannerProps>(function Banner({ bgVideo }) {
|
|||
|
||||
type SearchOverlayProps = {
|
||||
isOpen: boolean
|
||||
searchText: string
|
||||
onChange: (v: string) => void
|
||||
searchText?: string
|
||||
onChange?: (v: string) => void
|
||||
onClose: () => void
|
||||
onSearch: (v: string) => void
|
||||
}
|
||||
const SearchOverlay = memo<SearchOverlayProps>(function SearchOverlay({ isOpen, searchText, onChange, onClose }) {
|
||||
const SearchOverlay = memo<SearchOverlayProps>(function SearchOverlay({ isOpen, onChange, onClose, onSearch }) {
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const [searchText, setSearchText] = useState('')
|
||||
const handleTextChange = (text: string) => {
|
||||
setSearchText(text)
|
||||
onChange && onChange(text)
|
||||
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current)
|
||||
}
|
||||
|
||||
timerRef.current = setTimeout(() => {
|
||||
onSearch(text)
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// 删除定时器
|
||||
return () => {
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (!isOpen) return null
|
||||
return (
|
||||
<Block className="absolute left-0 right-0 top-0 z-50 mt-[24px] flex-row items-center gap-[8px] px-[20px]">
|
||||
|
|
@ -65,9 +90,9 @@ const SearchOverlay = memo<SearchOverlayProps>(function SearchOverlay({ isOpen,
|
|||
>
|
||||
<Ionicons name="search" size={20} color="black" style={{ marginRight: 8 }} />
|
||||
<TextInput
|
||||
// autoFocus
|
||||
autoFocus
|
||||
value={searchText}
|
||||
onChangeText={onChange}
|
||||
onChangeText={handleTextChange}
|
||||
placeholder="搜索作品 / 用户..."
|
||||
placeholderTextColor="#9CA3AF"
|
||||
style={{
|
||||
|
|
@ -85,6 +110,13 @@ const SearchOverlay = memo<SearchOverlayProps>(function SearchOverlay({ isOpen,
|
|||
>
|
||||
<Ionicons name="close" size={24} color="black" style={{ transform: [{ skewX: '6deg' }] }} />
|
||||
</Block>
|
||||
|
||||
<Block
|
||||
className="items-center justify-center border-[3px] border-black bg-[#FFE500] shadow-[4px_4px_0px_#000]"
|
||||
style={{ width: 48, height: 48, transform: [{ skewX: '-6deg' }] }}
|
||||
>
|
||||
<Text>登出</Text>
|
||||
</Block>
|
||||
</Block>
|
||||
)
|
||||
})
|
||||
|
|
@ -633,6 +665,11 @@ export default function Sync() {
|
|||
)
|
||||
}, [activeTab, isAuthenticated])
|
||||
|
||||
const handleSearch = useCallback((text: string) => {
|
||||
console.log('Search for:', text)
|
||||
// Implement your search logic here
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Block className="relative flex-1 flex-col overflow-visible bg-black px-[12px]">
|
||||
<Banner bgVideo={bgVideo} />
|
||||
|
|
@ -659,7 +696,7 @@ export default function Sync() {
|
|||
ListEmptyComponent={renderListEmpty}
|
||||
/>
|
||||
|
||||
<SearchOverlay isOpen={isSearchOpen} searchText={searchText} onChange={setSearchText} onClose={closeSearch} />
|
||||
<SearchOverlay isOpen={isSearchOpen} onClose={() => setIsSearchOpen(false)} onSearch={handleSearch} />
|
||||
</Block>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ import { useTemplateGenerations } from '@/hooks/data/use-template-generations'
|
|||
import { useFileUpload } from '@/hooks/actions/use-file-upload'
|
||||
import { useTemplateActions } from '@/hooks/actions/use-template-actions'
|
||||
import { useAuth } from '@/hooks/core/use-auth'
|
||||
import { aniStorage } from '@/utils/aniStorage'
|
||||
import { get } from 'react-native/Libraries/TurboModule/TurboModuleRegistry'
|
||||
|
||||
// ============ 常量定义 ============
|
||||
const BACKGROUND_VIDEOS = [
|
||||
|
|
@ -462,7 +464,15 @@ const Sync = () => {
|
|||
return !!connectedDevice?.id && !!selectedItem?.imageUrl
|
||||
}, [connectedDevice, selectedItem])
|
||||
|
||||
const handleSync = useCallback(() => {
|
||||
const handleSync = useCallback(async () => {
|
||||
// await aniStorage.set('test_url', { test: 'data' })
|
||||
console.log(
|
||||
'aniStorage.has----------------',
|
||||
await aniStorage.has('test_url'),
|
||||
// await aniStorage.delete('test_url'),
|
||||
// await aniStorage.get('test_url'),
|
||||
)
|
||||
|
||||
if (!canSync) {
|
||||
Toast.show({ title: '请先连接设备' })
|
||||
return
|
||||
|
|
@ -504,8 +514,12 @@ const Sync = () => {
|
|||
Toast.showLoading({ title: '上传中...', duration: 30e3 })
|
||||
|
||||
// 上传到云端
|
||||
const file = await fetch(uri).then((r) => r.blob())
|
||||
const { url, error } = await uploadFile(file, 'generations')
|
||||
const fileBlob = await fetch(uri).then((r) => r.blob())
|
||||
const mimeType = fileBlob.type || (isVideo ? 'video/mp4' : 'image/jpeg')
|
||||
|
||||
const file = new File([fileBlob], fileName, { type: mimeType })
|
||||
|
||||
const { url, error } = await uploadFile(file)
|
||||
|
||||
Toast.hideLoading()
|
||||
|
||||
|
|
|
|||
|
|
@ -78,10 +78,10 @@ function RootLayout() {
|
|||
return (
|
||||
<Providers>
|
||||
<Stack>
|
||||
<Stack.Screen name="auth" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="modal" options={{ headerShown: true }} />
|
||||
{/* <Stack.Screen name="pointList" options={{ headerShown: false }} /> */}
|
||||
<Stack.Screen name="auth" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="pointList" options={{ headerShown: false }} />
|
||||
</Stack>
|
||||
</Providers>
|
||||
)
|
||||
|
|
|
|||
315
app/auth.tsx
315
app/auth.tsx
|
|
@ -1,11 +1,11 @@
|
|||
import React, { useState, useMemo, useCallback } from 'react'
|
||||
import React, { useState, useCallback } from 'react'
|
||||
import { Block, Text, Toast, VideoBox } from '@share/components'
|
||||
import { Ionicons } from '@expo/vector-icons'
|
||||
import { ScrollView, Dimensions, TextInput, Platform } from 'react-native'
|
||||
import { Dimensions, TextInput, Platform } from 'react-native'
|
||||
import { useAuth } from '@/hooks/core/use-auth'
|
||||
import { KeyboardAvoidingView } from 'react-native-keyboard-controller'
|
||||
import { setAuthToken } from '@/lib/auth'
|
||||
|
||||
import { KeyboardAwareScrollView } from 'react-native-keyboard-controller'
|
||||
|
||||
const BACKGROUND_VIDEOS = [
|
||||
'https://cdn.roasmax.cn/material/b46f380532e14cf58dd350dbacc7c34a.mp4',
|
||||
|
|
@ -13,8 +13,6 @@ const BACKGROUND_VIDEOS = [
|
|||
'https://cdn.roasmax.cn/material/e4947477843f4067be7c37569a33d17b.mp4',
|
||||
]
|
||||
|
||||
|
||||
|
||||
type AuthMode = 'login' | 'register'
|
||||
|
||||
export default function Auth() {
|
||||
|
|
@ -28,8 +26,6 @@ export default function Auth() {
|
|||
const [confirmPassword, setConfirmPassword] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const { height: screenHeight } = Dimensions.get('window')
|
||||
|
||||
const handleLogin = useCallback(async () => {
|
||||
if (!username || !password) {
|
||||
Toast.show({ title: '请填写账号和密码' })
|
||||
|
|
@ -38,20 +34,23 @@ export default function Auth() {
|
|||
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await signIn.username({
|
||||
username,
|
||||
password,
|
||||
}, {
|
||||
onSuccess: async (ctx) => {
|
||||
const authToken = ctx.response.headers.get("set-auth-token");
|
||||
if (authToken) {
|
||||
setAuthToken(authToken)
|
||||
}
|
||||
const result = await signIn.username(
|
||||
{
|
||||
username,
|
||||
password,
|
||||
},
|
||||
onError: (ctx) => {
|
||||
console.error(`[LOGIN] login error`, ctx)
|
||||
}
|
||||
})
|
||||
{
|
||||
onSuccess: async (ctx) => {
|
||||
const authToken = ctx.response.headers.get('set-auth-token')
|
||||
if (authToken) {
|
||||
setAuthToken(authToken)
|
||||
}
|
||||
},
|
||||
onError: (ctx) => {
|
||||
console.error(`[LOGIN] login error`, ctx)
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
if (result.error) {
|
||||
Toast.show({ title: result.error.message || '登录失败' })
|
||||
|
|
@ -90,7 +89,7 @@ export default function Auth() {
|
|||
name: username,
|
||||
})
|
||||
|
||||
console.log('result------------', result);
|
||||
// console.log('result------------', result)
|
||||
|
||||
if (result.error) {
|
||||
Toast.show({ title: result.error.message || '注册失败' })
|
||||
|
|
@ -113,57 +112,106 @@ export default function Auth() {
|
|||
}, [mode, handleLogin, handleRegister])
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView behavior="padding" className="flex-1">
|
||||
<Block className="relative flex-1 bg-black">
|
||||
<Block className="absolute inset-[0px] z-[0] overflow-hidden">
|
||||
<VideoBox url={bgVideo} style={{ position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, opacity: 0.4 }} />
|
||||
<Block className="absolute inset-[0px] bg-black/20" />
|
||||
</Block>
|
||||
|
||||
<ScrollView className="relative z-[10] flex-1" contentContainerStyle={{ minHeight: screenHeight }} showsVerticalScrollIndicator={false}>
|
||||
<Block className="flex-1 items-center justify-center px-[24px] py-[40px]">
|
||||
<Block className="relative w-full max-w-[400px]">
|
||||
<Block className="relative mb-[32px] items-center">
|
||||
<Block className="h-[80px] w-[80px] items-center justify-center rounded-full border-[4px] border-black bg-accent shadow-deep-black">
|
||||
<Ionicons name="flash" size={40} color="black" />
|
||||
</Block>
|
||||
<Text className="font-900 mt-[16px] text-[32px] italic text-white">LOOMART</Text>
|
||||
<Block className="mt-[8px] h-[4px] w-[120px] bg-accent" />
|
||||
<Block className="relative flex-1 bg-black">
|
||||
<Block className="absolute inset-[0px] z-[0] overflow-hidden">
|
||||
<VideoBox url={bgVideo} style={{ position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, opacity: 0.4 }} />
|
||||
<Block className="absolute inset-[0px] bg-black/20" />
|
||||
</Block>
|
||||
<KeyboardAwareScrollView bottomOffset={100}>
|
||||
<Block className="flex-1 items-center justify-center px-[24px] py-[40px]">
|
||||
<Block className="relative w-full max-w-[400px]">
|
||||
<Block className="relative mb-[32px] items-center">
|
||||
<Block className="h-[80px] w-[80px] items-center justify-center rounded-full border-[4px] border-black bg-accent shadow-deep-black">
|
||||
<Ionicons name="flash" size={40} color="black" />
|
||||
</Block>
|
||||
<Text className="font-900 mt-[16px] text-[32px] italic text-white">LOOMART</Text>
|
||||
<Block className="mt-[8px] h-[4px] w-[120px] bg-accent" />
|
||||
</Block>
|
||||
|
||||
<Block className="mb-[24px] flex-row gap-[12px]">
|
||||
{(['login', 'register'] as const).map((tabMode) => {
|
||||
const isActive = mode === tabMode
|
||||
return (
|
||||
<Block
|
||||
key={tabMode}
|
||||
onClick={() => setMode(tabMode)}
|
||||
className={`flex-1 items-center justify-center border-[3px] border-black py-[12px] shadow-hard-black ${isActive ? 'bg-accent' : 'bg-white'}`}
|
||||
style={{ transform: [{ skewX: '-6deg' }] }}
|
||||
<Block className="mb-[24px] flex-row gap-[12px]">
|
||||
{(['login', 'register'] as const).map((tabMode) => {
|
||||
const isActive = mode === tabMode
|
||||
return (
|
||||
<Block
|
||||
key={tabMode}
|
||||
onClick={() => setMode(tabMode)}
|
||||
className={`flex-1 items-center justify-center border-[3px] border-black py-[12px] shadow-hard-black ${isActive ? 'bg-accent' : 'bg-white'}`}
|
||||
style={{ transform: [{ skewX: '-6deg' }] }}
|
||||
>
|
||||
<Text
|
||||
className={`font-900 text-[14px] italic ${isActive ? 'text-black' : 'text-gray-500'}`}
|
||||
style={{ transform: [{ skewX: '6deg' }] }}
|
||||
>
|
||||
<Text
|
||||
className={`font-900 text-[14px] italic ${isActive ? 'text-black' : 'text-gray-500'}`}
|
||||
style={{ transform: [{ skewX: '6deg' }] }}
|
||||
>
|
||||
{tabMode === 'login' ? '登录' : '注册'}
|
||||
</Text>
|
||||
</Block>
|
||||
)
|
||||
})}
|
||||
</Block>
|
||||
{tabMode === 'login' ? '登录' : '注册'}
|
||||
</Text>
|
||||
</Block>
|
||||
)
|
||||
})}
|
||||
</Block>
|
||||
|
||||
<Block className="relative border-[4px] border-black bg-white p-[24px] shadow-deep-black" style={{ transform: [{ skewX: '-3deg' }] }}>
|
||||
<Block style={{ transform: [{ skewX: '3deg' }] }}>
|
||||
<Block className="mt-[4px] gap-[16px]">
|
||||
{mode === 'login' ? (
|
||||
<Block className="relative border-[4px] border-black bg-white p-[24px] shadow-deep-black" style={{ transform: [{ skewX: '-3deg' }] }}>
|
||||
<Block style={{ transform: [{ skewX: '3deg' }] }}>
|
||||
<Block className="mt-[4px] gap-[16px]">
|
||||
{mode === 'login' ? (
|
||||
<Block>
|
||||
<Text className="font-900 mb-[8px] text-[12px] italic text-black">账号</Text>
|
||||
<Block
|
||||
className="flex-row items-center border-[3px] border-black bg-white px-[12px] shadow-medium-black"
|
||||
style={{ height: 48 }}
|
||||
>
|
||||
<Ionicons name="person-outline" size={20} color="black" style={{ marginRight: 8 }} />
|
||||
<TextInput
|
||||
value={username}
|
||||
onChangeText={setUsername}
|
||||
placeholder="用户名"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
autoCapitalize="none"
|
||||
style={{
|
||||
flex: 1,
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
color: 'black',
|
||||
}}
|
||||
/>
|
||||
</Block>
|
||||
</Block>
|
||||
) : (
|
||||
<>
|
||||
<Block>
|
||||
<Text className="font-900 mb-[8px] text-[12px] italic text-black">账号</Text>
|
||||
<Block className="flex-row items-center border-[3px] border-black bg-white px-[12px] shadow-medium-black" style={{ height: 48 }}>
|
||||
<Text className="font-900 mb-[8px] text-[12px] italic text-black">邮箱</Text>
|
||||
<Block
|
||||
className="flex-row items-center border-[3px] border-black bg-white px-[12px] shadow-medium-black"
|
||||
style={{ height: 48 }}
|
||||
>
|
||||
<Ionicons name="mail-outline" size={20} color="black" style={{ marginRight: 8 }} />
|
||||
<TextInput
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
placeholder="your@email.com"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
style={{
|
||||
flex: 1,
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
color: 'black',
|
||||
}}
|
||||
/>
|
||||
</Block>
|
||||
</Block>
|
||||
|
||||
<Block>
|
||||
<Text className="font-900 mb-[8px] text-[12px] italic text-black">用户名</Text>
|
||||
<Block
|
||||
className="flex-row items-center border-[3px] border-black bg-white px-[12px] shadow-medium-black"
|
||||
style={{ height: 48 }}
|
||||
>
|
||||
<Ionicons name="person-outline" size={20} color="black" style={{ marginRight: 8 }} />
|
||||
<TextInput
|
||||
value={username}
|
||||
onChangeText={setUsername}
|
||||
placeholder="用户名"
|
||||
placeholder="username"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
autoCapitalize="none"
|
||||
style={{
|
||||
|
|
@ -175,58 +223,40 @@ export default function Auth() {
|
|||
/>
|
||||
</Block>
|
||||
</Block>
|
||||
) : (
|
||||
<>
|
||||
<Block>
|
||||
<Text className="font-900 mb-[8px] text-[12px] italic text-black">邮箱</Text>
|
||||
<Block className="flex-row items-center border-[3px] border-black bg-white px-[12px] shadow-medium-black" style={{ height: 48 }}>
|
||||
<Ionicons name="mail-outline" size={20} color="black" style={{ marginRight: 8 }} />
|
||||
<TextInput
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
placeholder="your@email.com"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
style={{
|
||||
flex: 1,
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
color: 'black',
|
||||
}}
|
||||
/>
|
||||
</Block>
|
||||
</Block>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Block>
|
||||
<Text className="font-900 mb-[8px] text-[12px] italic text-black">用户名</Text>
|
||||
<Block className="flex-row items-center border-[3px] border-black bg-white px-[12px] shadow-medium-black" style={{ height: 48 }}>
|
||||
<Ionicons name="person-outline" size={20} color="black" style={{ marginRight: 8 }} />
|
||||
<TextInput
|
||||
value={username}
|
||||
onChangeText={setUsername}
|
||||
placeholder="username"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
autoCapitalize="none"
|
||||
style={{
|
||||
flex: 1,
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
color: 'black',
|
||||
}}
|
||||
/>
|
||||
</Block>
|
||||
</Block>
|
||||
</>
|
||||
)}
|
||||
<Block>
|
||||
<Text className="font-900 mb-[8px] text-[12px] italic text-black">密码</Text>
|
||||
<Block className="flex-row items-center border-[3px] border-black bg-white px-[12px] shadow-medium-black" style={{ height: 48 }}>
|
||||
<Ionicons name="lock-closed-outline" size={20} color="black" style={{ marginRight: 8 }} />
|
||||
<TextInput
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
placeholder="••••••••"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
secureTextEntry
|
||||
style={{
|
||||
flex: 1,
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
color: 'black',
|
||||
}}
|
||||
/>
|
||||
</Block>
|
||||
</Block>
|
||||
|
||||
{mode === 'register' && (
|
||||
<Block>
|
||||
<Text className="font-900 mb-[8px] text-[12px] italic text-black">密码</Text>
|
||||
<Block className="flex-row items-center border-[3px] border-black bg-white px-[12px] shadow-medium-black" style={{ height: 48 }}>
|
||||
<Text className="font-900 mb-[8px] text-[12px] italic text-black">确认密码</Text>
|
||||
<Block
|
||||
className="flex-row items-center border-[3px] border-black bg-white px-[12px] shadow-medium-black"
|
||||
style={{ height: 48 }}
|
||||
>
|
||||
<Ionicons name="lock-closed-outline" size={20} color="black" style={{ marginRight: 8 }} />
|
||||
<TextInput
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
value={confirmPassword}
|
||||
onChangeText={setConfirmPassword}
|
||||
placeholder="••••••••"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
secureTextEntry
|
||||
|
|
@ -239,57 +269,32 @@ export default function Auth() {
|
|||
/>
|
||||
</Block>
|
||||
</Block>
|
||||
)}
|
||||
|
||||
{mode === 'register' && (
|
||||
<Block>
|
||||
<Text className="font-900 mb-[8px] text-[12px] italic text-black">确认密码</Text>
|
||||
<Block className="flex-row items-center border-[3px] border-black bg-white px-[12px] shadow-medium-black" style={{ height: 48 }}>
|
||||
<Ionicons name="lock-closed-outline" size={20} color="black" style={{ marginRight: 8 }} />
|
||||
<TextInput
|
||||
value={confirmPassword}
|
||||
onChangeText={setConfirmPassword}
|
||||
placeholder="••••••••"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
secureTextEntry
|
||||
style={{
|
||||
flex: 1,
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
color: 'black',
|
||||
}}
|
||||
/>
|
||||
</Block>
|
||||
</Block>
|
||||
)}
|
||||
|
||||
<Block
|
||||
onClick={handleSubmit}
|
||||
className={`font-900 mt-[8px] flex-row items-center justify-center gap-[8px] border-[3px] border-black py-[14px] shadow-hard-black ${loading ? 'bg-gray-300' : 'bg-accent'}`}
|
||||
>
|
||||
{loading ? (
|
||||
<Ionicons name="hourglass-outline" size={20} color="black" />
|
||||
) : (
|
||||
<Ionicons name="flash" size={20} color="black" />
|
||||
)}
|
||||
<Text className="font-900 text-[16px] italic text-black">{loading ? '处理中...' : mode === 'login' ? '登录' : '注册'}</Text>
|
||||
</Block>
|
||||
|
||||
{mode === 'login' && (
|
||||
<Block className="mt-[8px] items-center">
|
||||
<Text className="font-700 text-[12px] text-gray-500">忘记密码?点击找回</Text>
|
||||
</Block>
|
||||
)}
|
||||
<Block
|
||||
onClick={handleSubmit}
|
||||
className={`font-900 mt-[8px] flex-row items-center justify-center gap-[8px] border-[3px] border-black py-[14px] shadow-hard-black ${loading ? 'bg-gray-300' : 'bg-accent'}`}
|
||||
>
|
||||
{loading ? <Ionicons name="hourglass-outline" size={20} color="black" /> : <Ionicons name="flash" size={20} color="black" />}
|
||||
<Text className="font-900 text-[16px] italic text-black">{loading ? '处理中...' : mode === 'login' ? '登录' : '注册'}</Text>
|
||||
</Block>
|
||||
|
||||
{mode === 'login' && (
|
||||
<Block className="mt-[8px] items-center">
|
||||
<Text className="font-700 text-[12px] text-gray-500">忘记密码?点击找回</Text>
|
||||
</Block>
|
||||
)}
|
||||
</Block>
|
||||
</Block>
|
||||
</Block>
|
||||
|
||||
<Block className="mt-[24px] items-center">
|
||||
<Text className="font-700 text-[12px] text-gray-400">© 2025 LOOMART. All rights reserved.</Text>
|
||||
</Block>
|
||||
<Block className="mt-[24px] items-center">
|
||||
<Text className="font-700 text-[12px] text-gray-400">© 2025 LOOMART. All rights reserved.</Text>
|
||||
</Block>
|
||||
</Block>
|
||||
</ScrollView>
|
||||
</Block>
|
||||
</KeyboardAvoidingView>
|
||||
</Block>
|
||||
<Block className="h-[200px]" />
|
||||
</KeyboardAwareScrollView>
|
||||
</Block>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,7 +134,11 @@ export default function ChargePage() {
|
|||
className="absolute bottom-0 left-0 right-0 border-t-[1px] border-white/5 bg-[#1C1E22]/95 px-[16px]"
|
||||
style={{ paddingBottom: insets.bottom + 12, paddingTop: 16 }}
|
||||
>
|
||||
<Block onClick={handlePay} opacity={0.8} className="h-[52px] items-center justify-center rounded-full bg-[#FFE500] shadow-lg shadow-[#FFE500]/20">
|
||||
<Block
|
||||
onClick={handlePay}
|
||||
opacity={0.8}
|
||||
className="h-[52px] items-center justify-center rounded-full bg-[#FFE500] shadow-lg shadow-[#FFE500]/20"
|
||||
>
|
||||
<Text className="text-[16px] font-[900] italic text-black">确认充值</Text>
|
||||
</Block>
|
||||
<Block className="mt-[12px] flex-row justify-center">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,139 @@
|
|||
import * as FileSystem from 'expo-file-system/legacy'
|
||||
|
||||
/**
|
||||
* 基于 Expo FileSystem 的持久化缓存系统,用于存储 .ani 文件数据
|
||||
* 键是 URL,值是 ani 文件内容
|
||||
*/
|
||||
|
||||
// 定义 ani 文件内容的类型
|
||||
type AniData = object | string
|
||||
|
||||
const CACHE_FOLDER = `${FileSystem.cacheDirectory}ani_cache/`
|
||||
|
||||
/**
|
||||
* 将 URL 转换为合法的文件名
|
||||
*
|
||||
* 为什么需要这样做?
|
||||
* 1. URL 中包含文件系统不允许的特殊字符(如 /, :, ?, & 等)。
|
||||
* 2. URL 的长度可能超过操作系统对文件名的长度限制(通常为 255 字符)。
|
||||
* 3. 简单的哈希算法可以将任意长度的 URL 转换为简短且唯一的标识符。
|
||||
*/
|
||||
const hashUrl = (url: string): string => {
|
||||
let hash = 0
|
||||
for (let i = 0; i < url.length; i++) {
|
||||
const char = url.charCodeAt(i)
|
||||
hash = (hash << 5) - hash + char
|
||||
hash |= 0 // Convert to 32bit integer
|
||||
}
|
||||
// 使用 hex 字符串和长度作为后缀来减少碰撞概率
|
||||
return `${hash.toString(16)}_${url.length}`
|
||||
}
|
||||
|
||||
class AniStorage {
|
||||
constructor() {
|
||||
this.ensureDir()
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保存储目录存在
|
||||
*/
|
||||
private async ensureDir() {
|
||||
try {
|
||||
// 直接尝试创建目录,intermediates: true 会处理父目录不存在的情况
|
||||
// 如果目录已存在,通常不会报错,或者我们在 catch 中忽略
|
||||
await FileSystem.makeDirectoryAsync(CACHE_FOLDER, { intermediates: true })
|
||||
} catch (error) {
|
||||
// 忽略目录已存在的错误
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存文件路径
|
||||
*/
|
||||
private getFilePath(url: string): string {
|
||||
const filename = hashUrl(url)
|
||||
return `${CACHE_FOLDER}${filename}.json`
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存中的 ani 数据
|
||||
* @param url 资源的 URL
|
||||
* @returns 缓存的数据,如果不存在则返回 undefined
|
||||
*/
|
||||
async get(url: string): Promise<AniData | undefined> {
|
||||
try {
|
||||
const filePath = this.getFilePath(url)
|
||||
// 移除 getInfoAsync 检查,直接尝试读取
|
||||
// 如果文件不存在,readAsStringAsync 会抛出错误,我们在 catch 中处理
|
||||
const content = await FileSystem.readAsStringAsync(filePath)
|
||||
try {
|
||||
return JSON.parse(content)
|
||||
} catch {
|
||||
return content
|
||||
}
|
||||
} catch (error) {
|
||||
// 文件不存在或读取失败时返回 undefined
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 ani 数据到缓存中
|
||||
* @param url 资源的 URL
|
||||
* @param data ani 文件数据
|
||||
*/
|
||||
async set(url: string, data: AniData): Promise<void> {
|
||||
try {
|
||||
await this.ensureDir()
|
||||
const filePath = this.getFilePath(url)
|
||||
const content = typeof data === 'string' ? data : JSON.stringify(data)
|
||||
await FileSystem.writeAsStringAsync(filePath, content)
|
||||
} catch (error) {
|
||||
console.warn('AniCache set error:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查缓存中是否存在该 URL (异步)
|
||||
* @param url 资源的 URL
|
||||
*/
|
||||
async has(url: string): Promise<boolean> {
|
||||
try {
|
||||
const filePath = this.getFilePath(url)
|
||||
// 由于 getInfoAsync 被弃用,这里尝试读取文件来检查是否存在
|
||||
await FileSystem.readAsStringAsync(filePath)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有缓存
|
||||
*/
|
||||
async clear(): Promise<void> {
|
||||
try {
|
||||
// idempotent: true 使得如果文件/目录不存在也不会报错
|
||||
await FileSystem.deleteAsync(CACHE_FOLDER, { idempotent: true })
|
||||
await this.ensureDir()
|
||||
} catch (error) {
|
||||
console.warn('AniCache clear error:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指定 URL 的缓存
|
||||
* @param url 资源的 URL
|
||||
*/
|
||||
async delete(url: string): Promise<void> {
|
||||
try {
|
||||
const filePath = this.getFilePath(url)
|
||||
await FileSystem.deleteAsync(filePath, { idempotent: true })
|
||||
} catch (error) {
|
||||
console.warn('AniCache delete error:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例实例
|
||||
export const aniStorage = new AniStorage()
|
||||
Loading…
Reference in New Issue