import { HeaderBar } from '@/components/ui/HeaderBar'; import { Colors } from '@/constants/Colors'; import { useMembershipModal } from '@/contexts/MembershipModalContext'; import { useAppDispatch } from '@/hooks/redux'; import { useAuthGuard } from '@/hooks/useAuthGuard'; import { useColorScheme } from '@/hooks/useColorScheme'; import { useCosUpload } from '@/hooks/useCosUpload'; import { useI18n } from '@/hooks/useI18n'; import { useVipService } from '@/hooks/useVipService'; import { recognizeFood } from '@/services/foodRecognition'; import { saveRecognitionResult, setError, setLoading } from '@/store/foodRecognitionSlice'; import { Ionicons } from '@expo/vector-icons'; import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect'; import { Image } from 'expo-image'; import { LinearGradient } from 'expo-linear-gradient'; import { useLocalSearchParams, useRouter } from 'expo-router'; import React, { useEffect, useState } from 'react'; import { ActivityIndicator, Alert, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import Animated, { Easing, FadeIn, SlideInDown, useAnimatedStyle, useSharedValue, withRepeat, withSequence, withTiming } from 'react-native-reanimated'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; export default function FoodRecognitionScreen() { const insets = useSafeAreaInsets(); const router = useRouter(); const { t } = useI18n(); const scheme = (useColorScheme() ?? 'light') as keyof typeof Colors; const colors = Colors[scheme]; const params = useLocalSearchParams<{ imageUri?: string; mealType?: string; }>(); const { imageUri, mealType } = params; const { upload } = useCosUpload(); const [showRecognitionProcess, setShowRecognitionProcess] = useState(false); const [recognitionLogs, setRecognitionLogs] = useState([]); const [currentStep, setCurrentStep] = useState<'idle' | 'uploading' | 'recognizing' | 'completed' | 'failed'>('idle'); const dispatch = useAppDispatch(); // Auth & VIP hooks const { ensureLoggedIn } = useAuthGuard(); const { handleServiceAccess } = useVipService(); const { openMembershipModal } = useMembershipModal(); // Animation values const progressValue = useSharedValue(0); const pulseValue = useSharedValue(1); useEffect(() => { if (currentStep === 'uploading') { progressValue.value = withTiming(0.4, { duration: 2000 }); startPulse(); } else if (currentStep === 'recognizing') { progressValue.value = withTiming(0.9, { duration: 3000 }); } else if (currentStep === 'completed') { progressValue.value = withTiming(1, { duration: 500 }); stopPulse(); } else if (currentStep === 'failed') { stopPulse(); } }, [currentStep]); const startPulse = () => { pulseValue.value = withRepeat( withSequence( withTiming(1.1, { duration: 800, easing: Easing.inOut(Easing.ease) }), withTiming(1, { duration: 800, easing: Easing.inOut(Easing.ease) }) ), -1, true ); }; const stopPulse = () => { pulseValue.value = withTiming(1); }; const addLog = (message: string) => { setRecognitionLogs(prev => [...prev, message]); }; const handleConfirm = async () => { if (!imageUri) return; const isLoggedIn = await ensureLoggedIn(); if (!isLoggedIn) return; const canAccess = handleServiceAccess( () => {}, // Allowed () => openMembershipModal() // Denied ); if (!canAccess) return; try { setShowRecognitionProcess(true); setRecognitionLogs([]); setCurrentStep('uploading'); dispatch(setLoading(true)); addLog(t('foodRecognition.logs.uploading')); const { url } = await upload( { uri: imageUri, name: 'food-image.jpg', type: 'image/jpeg' }, { prefix: 'food-images/' } ); addLog(t('foodRecognition.logs.uploadSuccess')); addLog(t('foodRecognition.logs.analyzing')); setCurrentStep('recognizing'); const recognitionResult = await recognizeFood({ imageUrls: [url] }); console.log('食物识别结果:', recognitionResult); if (!recognitionResult.isFoodDetected) { addLog(t('foodRecognition.logs.failed')); addLog(`💭 ${recognitionResult.nonFoodMessage || recognitionResult.analysisText}`); setCurrentStep('failed'); return; } addLog(t('foodRecognition.logs.analysisSuccess')); addLog(t('foodRecognition.logs.confidence', { value: recognitionResult.confidence })); addLog(t('foodRecognition.logs.itemsFound', { count: recognitionResult.items.length })); setCurrentStep('completed'); const recognitionId = `recognition_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; dispatch(saveRecognitionResult({ id: recognitionId, result: recognitionResult })); setTimeout(() => { router.replace(`/food/analysis-result?imageUri=${encodeURIComponent(url)}&mealType=${mealType}&recognitionId=${recognitionId}`); }, 1000); } catch (error) { console.warn('食物识别失败', error); addLog(t('foodRecognition.logs.error')); addLog(`💥 ${error instanceof Error ? error.message : t('foodRecognition.errors.unknown')}`); setCurrentStep('failed'); dispatch(setError(t('foodRecognition.errors.generic'))); } finally { dispatch(setLoading(false)); } }; const handleRetry = () => { setShowRecognitionProcess(false); setCurrentStep('idle'); setRecognitionLogs([]); dispatch(setError(null)); progressValue.value = 0; router.back(); }; const handleGoBack = () => { if (showRecognitionProcess && currentStep !== 'failed' && currentStep !== 'completed') { Alert.alert( t('foodRecognition.alerts.recognizing.title'), t('foodRecognition.alerts.recognizing.message'), [ { text: t('foodRecognition.alerts.recognizing.continue'), style: 'cancel' }, { text: t('foodRecognition.alerts.recognizing.back'), style: 'destructive', onPress: () => router.back() } ] ); } else { router.back(); } }; const pulseStyle = useAnimatedStyle(() => ({ transform: [{ scale: pulseValue.value }] })); const progressBarStyle = useAnimatedStyle(() => ({ width: `${progressValue.value * 100}%` })); if (!imageUri) { return ( {t('foodRecognition.errors.noImage')} ); } return ( {/* Image Preview Card */} {mealType && ( {getMealTypeLabel(mealType, t)} )} {/* Status / Action Area */} {!showRecognitionProcess ? ( {t('foodRecognition.info.title')} {t('foodRecognition.info.description')} {isLiquidGlassAvailable() ? ( {t('foodRecognition.actions.start')} ) : ( {t('foodRecognition.actions.start')} )} ) : ( {/* Progress Status Card */} {currentStep === 'completed' ? ( ) : currentStep === 'failed' ? ( ) : ( )} {getStatusTitle(currentStep, t)} {getStatusSubtitle(currentStep, t)} {/* Progress Bar */} {(currentStep === 'uploading' || currentStep === 'recognizing') && ( )} {/* Log Console */} {t('foodRecognition.actions.logs')} {recognitionLogs.map((log, idx) => ( {log} ))} {recognitionLogs.length === 0 && ( {t('foodRecognition.actions.logsPlaceholder')} )} {currentStep === 'failed' && ( {t('foodRecognition.actions.retry')} )} )} ); } function getMealTypeLabel(type: string, t: any): string { const map: Record = { breakfast: t('foodRecognition.mealTypes.breakfast'), lunch: t('foodRecognition.mealTypes.lunch'), dinner: t('foodRecognition.mealTypes.dinner'), snack: t('foodRecognition.mealTypes.snack'), }; return map[type] || t('foodRecognition.mealTypes.unknown'); } function getStatusColor(step: string, colors: any) { switch (step) { case 'completed': return colors.success; case 'failed': return colors.danger; default: return colors.primary; } } function getStatusTitle(step: string, t: any) { switch (step) { case 'idle': return t('foodRecognition.status.idle.title'); case 'uploading': return t('foodRecognition.status.uploading.title'); case 'recognizing': return t('foodRecognition.status.recognizing.title'); case 'completed': return t('foodRecognition.status.completed.title'); case 'failed': return t('foodRecognition.status.failed.title'); default: return t('foodRecognition.status.processing.title'); } } function getStatusSubtitle(step: string, t: any) { switch (step) { case 'uploading': return t('foodRecognition.status.uploading.subtitle'); case 'recognizing': return t('foodRecognition.status.recognizing.subtitle'); case 'completed': return t('foodRecognition.status.completed.subtitle'); case 'failed': return t('foodRecognition.status.failed.subtitle'); default: return t('foodRecognition.status.processing.subtitle'); } } const styles = StyleSheet.create({ container: { flex: 1, }, errorContainer: { flex: 1, alignItems: 'center', justifyContent: 'center', }, errorText: { fontSize: 16, color: '#64748b', }, contentContainer: { paddingHorizontal: 20, paddingBottom: 40, }, imageCard: { borderRadius: 24, overflow: 'hidden', backgroundColor: '#fff', shadowColor: '#0f172a', shadowOffset: { width: 0, height: 12 }, shadowOpacity: 0.08, shadowRadius: 24, elevation: 8, marginBottom: 24, }, imageFrame: { width: '100%', aspectRatio: 1, // Square image or 4:3 backgroundColor: '#f1f5f9', position: 'relative', }, mainImage: { width: '100%', height: '100%', }, mealBadge: { position: 'absolute', top: 16, right: 16, borderRadius: 16, overflow: 'hidden', }, mealBadgeGlass: { paddingHorizontal: 12, paddingVertical: 6, }, mealBadgeText: { color: '#fff', fontSize: 12, fontWeight: '600', }, infoSection: { alignItems: 'center', marginBottom: 32, paddingHorizontal: 10, }, iconCircle: { width: 56, height: 56, borderRadius: 28, backgroundColor: '#eff6ff', alignItems: 'center', justifyContent: 'center', marginBottom: 16, }, infoTitle: { fontSize: 22, fontWeight: '700', color: '#0f172a', marginBottom: 8, }, infoDesc: { fontSize: 15, color: '#64748b', textAlign: 'center', lineHeight: 22, }, actionButtons: { width: '100%', }, primaryButtonWrapper: { width: '100%', borderRadius: 20, overflow: 'hidden', shadowColor: '#0ea5e9', shadowOffset: { width: 0, height: 8 }, shadowOpacity: 0.25, shadowRadius: 16, elevation: 6, }, primaryButton: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: 18, backgroundColor: '#0284c7', // darker sky-600 }, primaryButtonText: { color: '#fff', fontSize: 17, fontWeight: '700', }, // Process styles processContainer: { width: '100%', }, progressCard: { backgroundColor: '#fff', borderRadius: 20, padding: 20, marginBottom: 16, shadowColor: '#0f172a', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.05, shadowRadius: 12, elevation: 3, }, progressHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 16, }, statusIconContainer: { marginRight: 16, }, statusIcon: { width: 48, height: 48, borderRadius: 24, alignItems: 'center', justifyContent: 'center', shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 6, }, statusTextContainer: { flex: 1, }, statusTitle: { fontSize: 17, fontWeight: '700', color: '#0f172a', marginBottom: 4, }, statusSubtitle: { fontSize: 13, color: '#64748b', }, progressBarBg: { height: 6, backgroundColor: '#f1f5f9', borderRadius: 3, overflow: 'hidden', }, progressBarFill: { height: '100%', borderRadius: 3, }, logsCard: { backgroundColor: 'rgba(255,255,255,0.6)', borderRadius: 16, padding: 16, borderWidth: 1, borderColor: 'rgba(255,255,255,0.8)', minHeight: 150, }, logsHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 12, opacity: 0.7, }, logsTitle: { fontSize: 13, fontWeight: '600', color: '#64748b', marginLeft: 6, }, logsScroll: { maxHeight: 200, }, logRow: { marginBottom: 6, }, logText: { fontSize: 13, color: '#334155', lineHeight: 18, fontFamily: 'Menlo', // Monospace if available }, logPlaceholder: { fontSize: 13, color: '#94a3b8', fontStyle: 'italic', textAlign: 'center', marginTop: 20, }, retryButton: { marginTop: 20, backgroundColor: '#fff', paddingVertical: 14, borderRadius: 16, alignItems: 'center', borderWidth: 1, borderColor: '#e2e8f0', }, retryButtonText: { color: '#0f172a', fontWeight: '600', fontSize: 15, }, });