import React, { useEffect, useMemo, useState, useRef, useCallback } from 'react'; import { Alert, FlatList, Image, Pressable, StyleSheet, Text, View, Platform, Dimensions } from 'react-native'; import { useTranslation } from 'react-i18next'; import { LinearGradient } from 'expo-linear-gradient'; import { Switch } from 'react-native'; import * as WebBrowser from 'expo-web-browser'; import Animated, { Easing, FadeIn, FadeOut, SlideInLeft, SlideInRight, SlideOutLeft, SlideOutRight, Layout, } from 'react-native-reanimated'; import SheetModal from '@/components/ui/SheetModal'; import { MOCK_CONTENT } from '@/src/constants/mockContent'; import { getDailyReminderSettings, getFavorites, setDailyReminderSettings, removeFavorite, getRecoFeedCache, getUserProfile, type DailyReminderSettings, type FavoriteItem, } from '@/src/storage/appStorage'; import AvatarIcon from '@/assets/images/home/Profile/Default_avatar.svg'; import MyLikeIcon from '@/assets/images/icon/mylike_icon.svg'; import SmallComponentIcon from '@/assets/images/icon/weight_icon.svg'; import RemindIcon from '@/assets/images/icon/Push_icon.svg'; import PrivacyIcon from '@/assets/images/icon/privacy_icon.svg'; import TermsIcon from '@/assets/images/icon/Terms_icon.svg'; import LanguageIcon from '@/assets/images/icon/language_icon.svg'; import SelectedIcon from '@/assets/images/icon/selected_icon.svg'; import QuestionIcon from '@/assets/images/home/Profile/widget/question_icon.svg'; import * as Notifications from 'expo-notifications'; import { changeLanguage } from '@/src/i18n'; import { fetchLegalLinks } from '@/src/services/legalApi'; import { getExpoPushTokenOrThrow, registerPushToken, setPushPreferences } from '@/src/services/pushApi'; const { width } = Dimensions.get('window'); type Props = { visible: boolean; name?: string; onClose: () => void; }; type Page = 'root' | 'favorites' | 'dailyReminder' | 'widget' | 'language' | 'widgetHowTo'; type NavDirection = 'forward' | 'back'; const NATURE_IMAGES = [ require('@/assets/theme/nature/1.png'), require('@/assets/theme/nature/2.png'), require('@/assets/theme/nature/3.png'), require('@/assets/theme/nature/4.png'), require('@/assets/theme/nature/5.png'), require('@/assets/theme/nature/6.png'), require('@/assets/theme/nature/7.png'), require('@/assets/theme/nature/8.png'), require('@/assets/theme/nature/9.png'), require('@/assets/theme/nature/10.png'), require('@/assets/theme/nature/11.png'), require('@/assets/theme/nature/12.png'), require('@/assets/theme/nature/13.png'), require('@/assets/theme/nature/14.png'), require('@/assets/theme/nature/15.png'), require('@/assets/theme/nature/17.png'), require('@/assets/theme/nature/18.png'), require('@/assets/theme/nature/19.png'), require('@/assets/theme/nature/20.png'), require('@/assets/theme/nature/22.png'), ]; export default function ProfileModal({ visible, name: propName, onClose }: Props) { const { t } = useTranslation(); const [page, setPage] = useState('root'); const [navDirection, setNavDirection] = useState('forward'); const [currentName, setCurrentName] = useState(propName); const [legalLinks, setLegalLinks] = useState<{ privacy?: string; terms?: string }>({}); const isRoot = page === 'root'; // 当弹窗打开时,尝试从存储中获取最新的昵称,确保与 onboarding 同步 useEffect(() => { if (visible) { getUserProfile().then(profile => { if (profile.name) { setCurrentName(profile.name); } }); // 打开弹窗时拉取协议链接(由后端按语言下发;默认 EN) fetchLegalLinks() .then((res) => { setLegalLinks({ privacy: res.privacyPolicyUrl, terms: res.termsOfUseUrl }); }) .catch((e) => { if (__DEV__) console.log('[LegalLinks] 拉取失败(ProfileModal):', e); setLegalLinks({}); }); } else { setNavDirection('back'); setPage('root'); } }, [visible]); // 同步外部 propName 的变化 useEffect(() => { if (propName) { setCurrentName(propName); } }, [propName]); function go(next: Page, direction: NavDirection) { setNavDirection(direction); setPage(next); } function handleClose() { setNavDirection('back'); setPage('root'); onClose(); } function handleSheetClose() { // 需求:二级页点击 X 等同于点击“返回” if (!isRoot) { go('root', 'back'); return; } handleClose(); } const openLink = useCallback( async (url?: string) => { if (!url) { Alert.alert(t('common.notice'), t('consent.linkUnavailable')); return; } try { await WebBrowser.openBrowserAsync(url); } catch (error) { Alert.alert(t('common.error'), t('common.openLinkError')); } }, [t], ); const title = useMemo(() => { if (page === 'favorites') return t('profile.favorites'); if (page === 'dailyReminder') return t('dailyReminder.title'); if (page === 'widget') return t('profile.widget'); if (page === 'language') return t('profile.language'); if (page === 'widgetHowTo') return t('widget.howToTitle'); return t('profile.title'); }, [page, t]); const transition = useMemo(() => { const duration = 220; const easing = Easing.out(Easing.cubic); // 需求:去掉左右滑动的切页动效,改为纯淡入淡出 const entering = FadeIn.duration(duration).easing(easing); const exiting = FadeOut.duration(duration).easing(easing); return { entering, exiting, }; }, [navDirection]); return ( {page === 'root' ? ( go('favorites', 'forward')} onOpenWidget={() => go('widget', 'forward')} onOpenDailyReminder={() => go('dailyReminder', 'forward')} onOpenLanguage={() => go('language', 'forward')} onOpenPrivacy={() => openLink(legalLinks.privacy)} onOpenTerms={() => openLink(legalLinks.terms)} /> ) : page === 'favorites' ? ( ) : page === 'dailyReminder' ? ( go('root', 'back')} /> ) : page === 'language' ? ( ) : page === 'widgetHowTo' ? ( ) : ( go('widgetHowTo', 'forward')} /> )} ); } function toastTodo(t: (key: string) => string) { Alert.alert(t('profile.todoTitle'), t('profile.todoDesc')); } function RootPage({ name, onOpenFavorites, onOpenWidget, onOpenDailyReminder, onOpenLanguage, onOpenPrivacy, onOpenTerms, }: { name?: string; onOpenFavorites: () => void; onOpenWidget: () => void; onOpenDailyReminder: () => void; onOpenLanguage: () => void; onOpenPrivacy: () => void; onOpenTerms: () => void; }) { const { t } = useTranslation(); return ( <> {name || 'Hali'} } title={t('profile.dailyReminder')} onPress={onOpenDailyReminder} /> } title={t('profile.privacy')} onPress={onOpenPrivacy} /> } title={t('profile.terms')} onPress={onOpenTerms} /> } title={t('profile.language')} onPress={onOpenLanguage} /> ); } function FavoritesPage({ visible, page }: { visible: boolean; page: Page }) { const { t } = useTranslation(); const [favorites, setFavorites] = useState<(FavoriteItem & { text: string })[]>([]); useEffect(() => { if (!visible) return; refreshFavorites(); }, [visible, page]); // 当页面切换回 favorites 时也刷新一次 async function refreshFavorites() { const storedFavs = await getFavorites(); const textMap = new Map(); // 1) Mock 文案 MOCK_CONTENT.forEach((c) => textMap.set(String(c.id), t(c.textKey))); // 2) 后端推荐缓存文案(避免收藏后 cache 覆盖就丢文案) const cache = await getRecoFeedCache(); cache?.items?.forEach((c) => textMap.set(String(c.content_id), c.text)); // 3) 组装:优先使用收藏时写入的 text,其次从 map 回填 const list = storedFavs.map((fav) => ({ ...fav, text: fav.text || textMap.get(String(fav.id)) || t('favorites.unknownText'), })); setFavorites(list); } async function handleRemove(favId: string) { // 1. 调用存储层移除收藏 await removeFavorite(favId); // 2. 更新本地状态 setFavorites(prev => prev.filter(item => item.favId !== favId)); } return ( {favorites.length === 0 ? ( {t('favorites.empty')} ) : ( it.favId} contentContainerStyle={styles.favList} showsVerticalScrollIndicator={false} renderItem={({ item }) => ( {item.date} {item.themeMode === 'scenery' ? ( ) : null} {item.text} handleRemove(item.favId)} style={styles.favRemoveBtn} hitSlop={10} > )} /> )} ); } function DailyReminderPage({ visible, onDone }: { visible: boolean; onDone: () => void }) { const { t } = useTranslation(); const [loading, setLoading] = useState(false); const [timesPerDay, setTimesPerDay] = useState(3); const [pushEnabled, setPushEnabled] = useState(false); const [hasSystemPermission, setHasSystemPermission] = useState(null); useEffect(() => { let cancelled = false; if (!visible) return; setLoading(true); (async () => { // 1. 获取本地存储设置 const s = await getDailyReminderSettings(); // 2. 获取系统通知权限(用于 UI 展示/引导) const settings = await Notifications.getPermissionsAsync(); const granted = settings.status === 'granted'; if (cancelled) return; setTimesPerDay(s.timesPerDay); // pushEnabled 表示用户意愿;若系统未授权则强制展示为关闭 setPushEnabled(Boolean(s.pushEnabled) && granted); setHasSystemPermission(granted); setLoading(false); })(); return () => { cancelled = true; }; }, [visible]); const handleTogglePush = async (value: boolean) => { if (value) { // 获取当前权限状态 const settings = await Notifications.getPermissionsAsync(); // 如果已经拒绝,弹窗提示 if (settings.status === 'denied') { Alert.alert( t('common.notice'), t('permissions.notificationsDenied') ); setPushEnabled(false); return; } // 尝试申请权限 const { status } = await Notifications.requestPermissionsAsync(); // 调试:打印状态 console.log('Push Permission Status:', status); if (status === 'granted') { setPushEnabled(true); setHasSystemPermission(true); // 获取 token 并上报后端(幂等) try { const expoPushToken = await getExpoPushTokenOrThrow(); await registerPushToken({ pushToken: expoPushToken }); // 偏好同步失败不应被用户感知为“开启失败” // (常见现象:后端已接收 token,但偏好接口短暂失败/超时) try { await setPushPreferences({ enabled: true, timesPerDay }); } catch (e) { const msg = e instanceof Error ? e.message : String(e); console.warn('[PushPreferences] 同步失败(ProfileModal,不阻塞)', msg); } } catch (e) { const msg = e instanceof Error ? e.message : String(e); Alert.alert(t('common.notice'), msg); } } else { setPushEnabled(false); setHasSystemPermission(false); Alert.alert(t('common.notice'), t('dailyReminder.permissionDenied')); } } else { setPushEnabled(false); // 关闭时尝试同步到后端(不阻塞) try { await setPushPreferences({ enabled: false, timesPerDay: 0 }); } catch { // ignore } } }; function clamp(next: number) { // 需求:0~5(0 表示关闭) return Math.min(5, Math.max(0, next)); } async function onOk() { if (loading) return; setLoading(true); const nextTimes = Math.min(5, Math.max(0, Math.round(timesPerDay))); const nextEnabled = Boolean(pushEnabled) && nextTimes > 0; const next: DailyReminderSettings = { timesPerDay: nextTimes, pushEnabled: nextEnabled }; await setDailyReminderSettings(next); // 同步后端偏好(幂等;失败不阻塞) try { await setPushPreferences({ enabled: nextEnabled, timesPerDay: nextTimes }); } catch (e) { const msg = e instanceof Error ? e.message : String(e); console.warn('[PushPreferences] 同步失败', msg); } setLoading(false); onDone(); } return ( <> setTimesPerDay((v) => clamp(v - 1))} hitSlop={10} style={styles.circleBtn} accessibilityRole="button" accessibilityLabel={t('dailyReminder.minus')} > {timesPerDay} {t('dailyReminder.timesUnit')} setTimesPerDay((v) => clamp(v + 1))} hitSlop={10} style={styles.circleBtn} accessibilityRole="button" accessibilityLabel={t('dailyReminder.plus')} > {t('dailyReminder.pushLabel')} {t('dailyReminder.ok')} ); } function WidgetPage({ onOpenHowTo }: { onOpenHowTo: () => void }) { const { t, i18n } = useTranslation(); const currentLang = i18n.language; // 需求:个人主页弹窗「小工具」页暂时隐藏锁屏小工具说明/入口 const showLockScreenWidget = false; // 根据语言选择图片 const widget1 = currentLang === 'en' ? require('@/assets/images/home/Profile/widget/Widget1_en.png') : require('@/assets/images/home/Profile/widget/Widget1_tw.png'); const widget2 = currentLang === 'en' ? require('@/assets/images/home/Profile/widget/Widget2_en.png') : require('@/assets/images/home/Profile/widget/Widget2_tw.png'); return ( {showLockScreenWidget ? ( {t('widget.lockScreen')} ) : null} {t('widget.homeScreen')} ); } function WidgetHowToPage() { const { t, i18n } = useTranslation(); const currentLang = i18n.language; const flatListRef = useRef(null); const [activeIndex, setActiveIndex] = useState(0); const [isManual, setIsManual] = useState(false); // React Native 环境下 setInterval 返回值类型与 Node 不同,这里用 ReturnType 兼容 const timerRef = useRef | null>(null); const images = currentLang === 'en' ? [ { id: '1', src: require('@/assets/images/home/Profile/widget/Widget_description1_en.png'), desc: t('widget.howToDesc1') }, { id: '2', src: require('@/assets/images/home/Profile/widget/Widget_description2_en.png'), desc: t('widget.howToDesc2') }, ] : [ { id: '1', src: require('@/assets/images/home/Profile/widget/Widget_description1_tw.png'), desc: t('widget.howToDesc1') }, { id: '2', src: require('@/assets/images/home/Profile/widget/Widget_description2_tw.png'), desc: t('widget.howToDesc2') }, ]; const startAutoPlay = useCallback(() => { if (timerRef.current) clearInterval(timerRef.current); timerRef.current = setInterval(() => { if (!isManual) { const nextIndex = (activeIndex + 1) % images.length; flatListRef.current?.scrollToIndex({ index: nextIndex, animated: true }); setActiveIndex(nextIndex); } }, 3000); }, [activeIndex, isManual, images.length]); useEffect(() => { startAutoPlay(); return () => { if (timerRef.current) clearInterval(timerRef.current); }; }, [startAutoPlay]); const onScroll = (event: any) => { const x = event.nativeEvent.contentOffset.x; const index = Math.round(x / (width - 32)); if (index !== activeIndex) { setActiveIndex(index); } }; const onScrollBeginDrag = () => { setIsManual(true); if (timerRef.current) clearInterval(timerRef.current); }; return ( item.id} horizontal pagingEnabled showsHorizontalScrollIndicator={false} onScroll={onScroll} onScrollBeginDrag={onScrollBeginDrag} scrollEventThrottle={16} renderItem={({ item }) => ( {item.desc} )} /> {images.map((_, i) => ( ))} ); } function LanguagePage() { const { t, i18n } = useTranslation(); const currentLang = i18n.language; const languages = [ { id: 'zh-TW', label: t('language.zhTW') }, { id: 'en', label: t('language.en') }, ]; return ( {languages.map((lang, index) => ( changeLanguage(lang.id as any)} > {lang.label} {currentLang === lang.id && ( )} ))} ); } function QuickCard({ title, onPress, children, }: { title: string; onPress: () => void; children: React.ReactNode; }) { return ( {title} {children} ); } function ListItem({ icon, title, onPress, }: { icon: React.ReactNode; title: string; onPress: () => void; }) { return ( {icon} {title} ); } const styles = StyleSheet.create({ pageWrap: { // 给页面切换动画一个稳定的容器,避免布局抖动 }, backRow: { alignSelf: 'flex-start', paddingVertical: 4, paddingHorizontal: 2, marginBottom: 6, }, backText: { color: 'rgba(94,42,40,0.75)', fontSize: 13, fontWeight: '700', }, header: { alignItems: 'center', paddingTop: 6, paddingBottom: 14, }, // 头像是默认图片:234x183,没有圆角/边框/背景颜色 avatarImage: { width: 234, height: 183, marginBottom: 10, }, name: { color: '#5E2A28', fontSize: 18, fontWeight: '700', }, quickRow: { flexDirection: 'row', gap: 12, paddingBottom: 14, }, quickCard: { flex: 1, borderRadius: 20, padding: 14, backgroundColor: 'rgba(244,214,194,0.65)', height: 142, justifyContent: 'space-between', }, quickTitle: { color: '#482A0D', fontSize: 15, fontWeight: '500', }, quickIconWrap: { alignItems: 'flex-end', justifyContent: 'flex-end', flex: 1 }, list: { borderRadius: 20, backgroundColor: 'rgba(255,255,255,0.75)', overflow: 'hidden', marginBottom: 8, }, item: { height: 52, paddingHorizontal: 18, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: 'rgba(94,42,40,0.10)', }, itemLeft: { flexDirection: 'row', alignItems: 'center', gap: 12, }, itemIcon: { width: 24, height: 24, alignItems: 'center', justifyContent: 'center', }, itemText: { color: '#522B09', fontSize: 15, fontWeight: '500', }, chevron: { color: '#C7B4A1', fontSize: 20, marginTop: -2, }, favContainer: { flex: 1, minHeight: 500, // 确保容器有足够高度 }, favEmpty: { color: 'rgba(94,42,40,0.55)', fontSize: 15, textAlign: 'center', paddingVertical: 100, }, favList: { paddingHorizontal: 20, paddingBottom: 80, }, favCard: { flexDirection: 'row', alignItems: 'center', marginBottom: 24, }, favLeft: { width: 90, }, favDate: { fontSize: 14, color: '#772F00', fontFamily: Platform.OS === 'ios' ? 'STIX Two Text' : 'serif', fontWeight: '600', }, favRight: { flex: 1, }, favThumb: { backgroundColor: '#FFF4EA', borderRadius: 16, padding: 20, width: width * 0.6, height: 161, justifyContent: 'center', position: 'relative', borderWidth: 1, borderColor: 'rgba(119, 47, 0, 0.05)', overflow: 'hidden', }, favThumbText: { fontSize: 15, lineHeight: 22, color: '#5E2A28', fontWeight: '500', textAlign: 'center', }, favRemoveBtn: { position: 'absolute', top: 15, right: 15, width: 37, height: 33, alignItems: 'center', justifyContent: 'center', }, counter: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 22, paddingTop: 8, paddingBottom: 22, }, circleBtn: { width: 42, height: 42, borderRadius: 21, backgroundColor: 'rgba(244,214,194,0.75)', alignItems: 'center', justifyContent: 'center', }, circleText: { color: '#5E2A28', fontSize: 20, fontWeight: '700', marginTop: -1, }, countCenter: { flexDirection: 'row', alignItems: 'flex-end', gap: 6, }, countNumber: { color: '#111', fontSize: 54, fontWeight: '800', letterSpacing: -1, }, countUnit: { color: '#111', fontSize: 16, fontWeight: '700', marginBottom: 10, }, remindRow: { height: 54, borderRadius: 20, backgroundColor: 'rgba(255,255,255,0.75)', paddingHorizontal: 14, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 18, width: width - 40, // 屏幕宽度减去左右各 20pt alignSelf: 'center', }, rowLeft: { flexDirection: 'row', alignItems: 'center', gap: 10 }, rowRight: { height: '100%', justifyContent: 'center', alignItems: 'center', }, rowIcon: { width: 28, height: 28, borderRadius: 10, backgroundColor: 'rgba(244,214,194,0.55)', alignItems: 'center', justifyContent: 'center', }, rowText: { color: '#5E2A28', fontSize: 15, fontWeight: '600', }, okPressable: { marginBottom: 6, marginTop: 40, // 增加顶部间距以撑开高度 }, okBtn: { height: 52, borderRadius: 26, alignItems: 'center', justifyContent: 'center', }, okBtnDisabled: { opacity: 0.7 }, okText: { color: '#fff', fontSize: 16, fontWeight: '700', }, widgetContent: { flex: 1, paddingTop: 10, }, questionBtn: { position: 'absolute', right: 20, top: -34, // 放在标题栏右侧 zIndex: 10, }, widgetScroll: { alignItems: 'center', gap: 30, paddingBottom: 40, }, widgetItem: { alignItems: 'center', width: '100%', }, widgetImg1: { width: width * 0.9, height: (width * 0.9) * (156 / 311), }, widgetImg2: { width: width * 0.9, height: (width * 0.9) * (175 / 311), }, widgetLabel: { marginTop: 12, fontSize: 15, color: 'rgba(94, 42, 40, 0.45)', fontWeight: '500', }, howToPage: { flex: 1, alignItems: 'center', paddingTop: 20, }, howToSlide: { width: width - 32, // 减去 SheetModal 的 paddingHorizontal: 16 * 2 alignItems: 'center', }, howToImg: { width: width * 0.9, height: (width * 0.9) * (234 / 326), marginBottom: 40, }, howToDesc: { fontSize: 15, lineHeight: 25, color: '#522B09', textAlign: 'center', fontWeight: '500', paddingHorizontal: 20, }, pagination: { flexDirection: 'row', position: 'absolute', top: (width * 0.9) * (234 / 326) + 35, // 根据新的图片高度动态计算 gap: 8, }, dot: { width: 8, height: 8, borderRadius: 4, }, dotActive: { backgroundColor: '#EEB054', }, dotInactive: { backgroundColor: 'rgba(238, 176, 84, 0.3)', }, langPage: { paddingHorizontal: 20, paddingTop: 10, }, langList: { backgroundColor: '#FFFFFF', borderRadius: 20, overflow: 'hidden', width: width - 40, // 屏幕宽度减去左右各 20pt alignSelf: 'center', }, langItem: { height: 62, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, }, langItemBorder: { borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: 'rgba(94,42,40,0.1)', }, langText: { fontSize: 15, color: '#522B09', fontWeight: '500', textTransform: 'capitalize', }, });