diff --git a/README.md b/README.md index b989173..179a02c 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,17 @@ 本项目面向宝妈群体,提供情绪价值与正念练习支持。 客户端采用 React Native + Expo,后端采用 Python + FastAPI,数据库 MySQL,任务调度 Celery,推送/定时等功能完整支持。 +# git提交 +git add . +git commit -m '备注' +git push origin + +获取最新分支 +git pull + +# 创建自己的分支 +git checkout -b 姓名拼写 + # 目录结构 /mindfulness diff --git a/client/app.json b/client/app.json index 6c4afd0..00ddbaf 100644 --- a/client/app.json +++ b/client/app.json @@ -1,11 +1,11 @@ { "expo": { - "name": "client", - "slug": "client", + "name": "Hey Mama", + "slug": "hey-mama", "version": "1.0.0", "orientation": "portrait", "icon": "./assets/images/icon.png", - "scheme": "client", + "scheme": "heymama", "userInterfaceStyle": "automatic", "newArchEnabled": true, "splash": { @@ -15,7 +15,7 @@ }, "ios": { "supportsTablet": true, - "bundleIdentifier": "com.anonymous.client" + "bundleIdentifier": "com.heymama.app" }, "android": { "adaptiveIcon": { diff --git a/client/app/(app)/home.tsx b/client/app/(app)/home.tsx index 9a81838..c0fb964 100644 --- a/client/app/(app)/home.tsx +++ b/client/app/(app)/home.tsx @@ -1,8 +1,7 @@ -import { useEffect, useLayoutEffect, useMemo, useState } from 'react'; -import { StyleSheet, View } from 'react-native'; -import { Pressable } from 'react-native'; +import { useEffect, useLayoutEffect, useMemo, useState, useCallback, useRef } from 'react'; +import { StyleSheet, View, Dimensions, Text, Pressable, PanResponder, Animated as RNAnimated } from 'react-native'; import { useTranslation } from 'react-i18next'; -import { useNavigation } from 'expo-router'; +import { useNavigation, useFocusEffect } from 'expo-router'; import Animated, { Easing, runOnJS, @@ -27,9 +26,10 @@ import ThemeModal from '@/components/home/ThemeModal'; import ThemeIcon from '@/assets/images/home/theme.svg'; import MyIcon from '@/assets/images/home/my.svg'; -import LikeOutlineIcon from '@/assets/images/home/like.svg'; import LikeFilledIcon from '@/assets/images/home/like_filled.svg'; -import HateIcon from '@/assets/images/home/hate.svg'; +import LikeIcon from '@/assets/images/icon/like_icon.svg'; + +const { height: SCREEN_HEIGHT } = Dimensions.get('window'); export default function HomeScreen() { const { t } = useTranslation(); @@ -44,19 +44,27 @@ export default function HomeScreen() { const item = useMemo(() => MOCK_CONTENT[index % MOCK_CONTENT.length], [index]); - useEffect(() => { - let cancelled = false; - (async () => { - const mode = await getThemeMode(); - const profile = await getUserProfile(); - if (cancelled) return; - setThemeModeState(mode); - setProfileName(profile.name); - })(); - return () => { - cancelled = true; - }; - }, []); + // 动画相关 Shared Values + const translateY = useSharedValue(0); + const opacity = useSharedValue(1); + const likeScale = useSharedValue(1); + + // 每次进入页面或页面获得焦点时刷新个人信息 + useFocusEffect( + useCallback(() => { + let cancelled = false; + (async () => { + const mode = await getThemeMode(); + const profile = await getUserProfile(); + if (cancelled) return; + setThemeModeState(mode); + setProfileName(profile.name); + })(); + return () => { + cancelled = true; + }; + }, []) + ); const backgroundColor = themeMode === 'color' ? '#F3D0E1' : '#F4D6C2'; @@ -83,60 +91,109 @@ export default function HomeScreen() { }); }, [backgroundColor, navigation, t]); - const likeScale = useSharedValue(1); - const hateScale = useSharedValue(1); + const textAnimatedStyle = useAnimatedStyle(() => ({ + transform: [{ translateY: translateY.value }], + opacity: opacity.value, + })); const likeAnimatedStyle = useAnimatedStyle(() => ({ transform: [{ scale: likeScale.value }], })); - const hateAnimatedStyle = useAnimatedStyle(() => ({ - transform: [{ scale: hateScale.value }], - })); + // 切换到下一条文案的统一动画逻辑 + const triggerNextContent = useCallback(() => { + if (busy) return; + setBusy(true); - function playLikeAnimationAndThen(next: () => void) { + // 1. 当前文案向上移动并消失 + translateY.value = withTiming(-40, { duration: 300, easing: Easing.out(Easing.quad) }); + opacity.value = withTiming(0, { duration: 300 }, (finished) => { + if (finished) { + // 2. 切换数据索引 + runOnJS(setIndex)(index + 1); + runOnJS(setLikeFilled)(false); + + // 3. 准备下一条文案:先瞬移到下方 40pt + translateY.value = 40; + + // 4. 下一条文案向上移动到原位并显现 + translateY.value = withTiming(0, { duration: 400, easing: Easing.out(Easing.back(1)) }); + opacity.value = withTiming(1, { duration: 400 }, (finished) => { + if (finished) { + runOnJS(setBusy)(false); + } + }); + } + }); + }, [busy, index, translateY, opacity]); + + const lastTapRef = useRef(0); + + // 使用 Ref 解决 PanResponder 闭包陷阱,确保手势回调能拿到最新的 state 和 function + const handlersRef = useRef({ onPressLike, triggerNextContent }); + useEffect(() => { + handlersRef.current = { onPressLike, triggerNextContent }; + }, [onPressLike, triggerNextContent]); + + // 使用系统自带的 PanResponder 代替第三方手势库 + const panResponder = useRef( + PanResponder.create({ + onStartShouldSetPanResponder: () => true, // 允许开始捕获 + onMoveShouldSetPanResponder: (_, gestureState) => { + // 只有当垂直滑动距离大于 20 时才接管位移手势 + return Math.abs(gestureState.dy) > 20; + }, + onPanResponderRelease: (_, gestureState) => { + const now = Date.now(); + const DOUBLE_TAP_DELAY = 300; + + // 1. 双击逻辑判定 + if (now - lastTapRef.current < DOUBLE_TAP_DELAY) { + // 判定为双击 + if (Math.abs(gestureState.dx) < 10 && Math.abs(gestureState.dy) < 10) { + runOnJS(handlersRef.current.onPressLike)(); + lastTapRef.current = 0; // 重置 + return; + } + } + lastTapRef.current = now; + + // 2. 上滑逻辑判定 + if (gestureState.dy < -50) { // 上滑超过 50pt + runOnJS(handlersRef.current.triggerNextContent)(); + } + }, + }) + ).current; + + async function onPressLike() { + if (busy) return; setLikeFilled(true); + + // 1. 获取当前日期 + const now = new Date(); + const dateStr = `${now.getFullYear()}.${String(now.getMonth() + 1).padStart(2, '0')}.${String(now.getDate()).padStart(2, '0')}`; + + // 2. 保存到收藏夹,包含当前背景信息 + await addFavorite({ + id: item.id, + date: dateStr, + themeMode: themeMode, + background: backgroundColor, // 目前存储的是颜色值 + }); + + // 3. 爱心缩放动画 likeScale.value = withSequence( - withTiming(0.92, { duration: 90, easing: Easing.out(Easing.cubic) }), - withTiming(1.14, { duration: 120, easing: Easing.out(Easing.cubic) }), - withTiming(1, { duration: 140, easing: Easing.out(Easing.cubic) }, (finished) => { - if (finished) runOnJS(next)(); + withTiming(0.8, { duration: 100 }), + withTiming(1.2, { duration: 150 }), + withTiming(1, { duration: 100 }, (finished) => { + if (finished) { + runOnJS(triggerNextContent)(); + } }) ); } - function playHateAnimationAndThen(next: () => void) { - hateScale.value = withSequence( - withTiming(0.92, { duration: 90, easing: Easing.out(Easing.cubic) }), - withTiming(1.06, { duration: 110, easing: Easing.out(Easing.cubic) }), - withTiming(1, { duration: 120, easing: Easing.out(Easing.cubic) }, (finished) => { - if (finished) runOnJS(next)(); - }) - ); - } - - function onPressLike() { - if (busy) return; - setBusy(true); - playLikeAnimationAndThen(async () => { - await setReaction(item.id, 'like'); - await addFavorite(item.id); - setIndex((i) => i + 1); - setTimeout(() => setLikeFilled(false), 220); - setBusy(false); - }); - } - - function onPressHate() { - if (busy) return; - setBusy(true); - playHateAnimationAndThen(async () => { - await setReaction(item.id, 'dislike'); - setIndex((i) => i + 1); - setBusy(false); - }); - } - async function onSelectTheme(next: ThemeMode) { setThemeModeState(next); await setThemeMode(next); @@ -144,40 +201,24 @@ export default function HomeScreen() { } return ( - - - {item.text} - + + + {item.text} + - - (hateScale.value = withTiming(0.92, { duration: 80 }))} - onPressOut={() => (hateScale.value = withTiming(1, { duration: 120 }))} - accessibilityRole="button" - accessibilityLabel={t('home.dislike')} - hitSlop={10} - style={styles.reactionInner} - > - - - - (likeScale.value = withTiming(0.92, { duration: 80 }))} - onPressOut={() => (likeScale.value = withTiming(1, { duration: 120 }))} accessibilityRole="button" accessibilityLabel={t('home.like')} - hitSlop={10} + hitSlop={20} style={styles.reactionInner} > {likeFilled ? ( - + ) : ( - + )} @@ -220,7 +261,7 @@ const styles = StyleSheet.create({ flex: 1, padding: 20, justifyContent: 'center', - gap: 16, + alignItems: 'center', }, headerRight: { flexDirection: 'row', @@ -236,37 +277,31 @@ const styles = StyleSheet.create({ justifyContent: 'center', }, card: { - borderRadius: 16, - padding: 20, - backgroundColor: 'rgba(255,255,255,0.65)', - borderWidth: StyleSheet.hairlineWidth, - borderColor: '#E5E7EB', + paddingHorizontal: 30, + alignItems: 'center', + justifyContent: 'center', }, text: { - fontSize: 20, - lineHeight: 28, + fontSize: 22, + lineHeight: 32, color: '#5E2A28', fontWeight: '700', + textAlign: 'center', }, actions: { + position: 'absolute', + bottom: SCREEN_HEIGHT * 0.16, + left: 0, + right: 0, flexDirection: 'row', - gap: 12, justifyContent: 'center', }, reactionButton: { - width: 58, - height: 58, - borderRadius: 29, - backgroundColor: 'rgba(255,255,255,0.75)', alignItems: 'center', justifyContent: 'center', }, reactionInner: { - width: 58, - height: 58, - borderRadius: 29, alignItems: 'center', justifyContent: 'center', }, }); - diff --git a/client/app/(onboarding)/onboarding.tsx b/client/app/(onboarding)/onboarding.tsx index e95d54a..df0ef33 100644 --- a/client/app/(onboarding)/onboarding.tsx +++ b/client/app/(onboarding)/onboarding.tsx @@ -1,66 +1,147 @@ import { useState } from 'react'; import { useRouter } from 'expo-router'; +import * as Notifications from 'expo-notifications'; import { OnboardingLayout } from '@/components/onboarding/OnboardingLayout'; import { NameInputStep } from '@/components/onboarding/NameInputStep'; -import { IntentSelectionStep } from '@/components/onboarding/IntentSelectionStep'; -import { setOnboardingCompleted, setUserProfile } from '@/src/storage/appStorage'; +import { SelectionStep } from '@/components/onboarding/SelectionStep'; +import { ReminderStep } from '@/components/onboarding/ReminderStep'; +import { setOnboardingCompleted, setUserProfile, setDailyReminderSettings } from '@/src/storage/appStorage'; + +const STEPS = [ + { id: 'name', type: 'name', title: '我可以怎么称呼你?' }, + { + id: 'status', + type: 'selection', + title: '媽媽的狀態?', + options: [ + { id: 'pregnant', label: '懷孕中/準備成為媽媽' }, + { id: 'has_kids', label: '已經有孩子' }, + { id: 'no_fill', label: '不想填寫' }, + ] + }, + { + id: 'emotion', + type: 'selection', + title: '當下情緒狀態?', + options: [ + { id: 'happy', label: '愉悅、滿足' }, + { id: 'calm', label: '平靜、安穩' }, + { id: 'stressed', label: '被壓得有點喘不過氣' }, + { id: 'low', label: '情緒低落' }, + ] + }, + { + id: 'influence', + type: 'selection', + title: '是什麼影響了你最近的感受?', + options: [ + { id: 'family', label: '家庭與孩子' }, + { id: 'work', label: '工作或學習' }, + { id: 'relationship', label: '親密關係' }, + { id: 'friends', label: '朋友與人際' }, + { id: 'health', label: '身心健康' }, + ] + }, + { + id: 'support', + type: 'selection', + title: '最需要什麼支持?', + options: [ + { id: 'emotional', label: '情緒支持' }, + { id: 'parenting', label: '育兒壓力' }, + { id: 'self_worth', label: '自我價值' }, + { id: 'anxiety', label: '焦慮舒緩' }, + { id: 'balance', label: '休息與平衡' }, + ] + }, + { id: 'reminder', type: 'reminder', title: '你需要每天几次提醒?' }, +]; export default function OnboardingScreen() { const router = useRouter(); - const [step, setStep] = useState(0); + const [stepIndex, setStepIndex] = useState(0); const [name, setName] = useState(''); - const [intents, setIntents] = useState([]); + const [selections, setSelections] = useState>({}); + const [reminderTimes, setReminderTimes] = useState(3); + + const currentStep = STEPS[stepIndex]; async function onFinish() { - // Save whatever input we have - await setUserProfile({ name, intents }); + // 请求推送权限 + const { status } = await Notifications.requestPermissionsAsync(); + const pushEnabled = status === 'granted'; + + await setUserProfile({ + name, + intents: Object.values(selections).flat() + }); + await setDailyReminderSettings({ + timesPerDay: reminderTimes, + pushEnabled: pushEnabled + }); await setOnboardingCompleted(true); - router.replace('/(onboarding)/push-prompt'); + router.replace('/(app)/home'); } - async function onNext() { - if (step === 0) { - setStep(1); + const onNext = () => { + if (stepIndex < STEPS.length - 1) { + setStepIndex(stepIndex + 1); } else { - await onFinish(); + onFinish(); } - } + }; - async function onSkip() { - // Skip logic: move to next step regardless of input - if (step === 0) { - setStep(1); - } else { - await onFinish(); + const onBack = () => { + if (stepIndex > 0) { + setStepIndex(stepIndex - 1); } - } + }; - // Step 0: Next button requires input - // Step 1: Next button always enabled (can proceed with empty selection) - const nextEnabled = step === 0 ? name.trim().length > 0 : true; + const onSkip = () => { + router.replace('/(app)/home'); + }; + + const handleToggleSelection = (id: string) => { + setSelections(prev => { + const currentIds = prev[currentStep.id] || []; + const nextIds = currentIds.includes(id) + ? currentIds.filter(i => i !== id) + : [...currentIds, id]; + return { ...prev, [currentStep.id]: nextIds }; + }); + }; return ( 0} > - {step === 0 ? ( + {currentStep.type === 'name' && ( 0 ? onNext : undefined} + onNext={onNext} /> - ) : ( - { - setIntents(prev => - prev.includes(id) - ? prev.filter(i => i !== id) - : [...prev, id] - ); - }} + )} + + {currentStep.type === 'selection' && ( + + )} + + {currentStep.type === 'reminder' && ( + )} diff --git a/client/app/(splash)/splash.tsx b/client/app/(splash)/splash.tsx index b59e3a6..dd2190b 100644 --- a/client/app/(splash)/splash.tsx +++ b/client/app/(splash)/splash.tsx @@ -1,12 +1,15 @@ import React, { useEffect, useState } from 'react'; -import { View, Text, Image, StyleSheet, TouchableOpacity, Dimensions, Platform, Alert } from 'react-native'; -import { LinearGradient } from 'expo-linear-gradient'; +import { View, Text, StyleSheet, TouchableOpacity, Dimensions, Platform, Alert, Image } from 'react-native'; import { useRouter } from 'expo-router'; import * as WebBrowser from 'expo-web-browser'; import { useTranslation } from 'react-i18next'; import { SafeAreaView } from 'react-native-safe-area-context'; import { setConsentAccepted, getConsentAccepted } from '../../src/storage/appStorage'; +// 导入 SVG 组件 +import FlowersBg from '../../assets/images/index/flowers_endbg.svg'; +import WelcomeBtn from '../../assets/images/index/welcome_btn.svg'; + const { width, height } = Dimensions.get('window'); export default function SplashScreen() { @@ -22,18 +25,15 @@ export default function SplashScreen() { const accepted = await getConsentAccepted(); setShowConsent(!accepted); if (accepted) { - // 如果已经同意过,直接跳转到首页分发 - // 注意:这里需要与 app/index.tsx 配合,如果 app/index.tsx 已经判断了 consent=true 不会跳过来, - // 那么这里其实是防守。 - // 但如果用户是通过 deep link 或其他方式强制进入 splash,这个逻辑会把他们送走。 - router.replace('/'); + router.replace('/'); } }; const handleAgree = async () => { await setConsentAccepted(true); setShowConsent(false); - router.replace('/'); + // 跳转到 onboarding 流程 + router.replace('/(onboarding)/onboarding'); }; const openLink = async (url: string) => { @@ -44,31 +44,40 @@ export default function SplashScreen() { } }; + const bgDecorationTop = 363; + const bgDecorationHeight = height * 0.6; + const contentTop = bgDecorationTop + (bgDecorationHeight * 0.25); + return ( - + {/* 中间的背景装饰 SVG (现在放在上面,作为上层) */} + + + - - {t('consent.title')} - {t('consent.subtitle')} + {/* 顶部的花图片 (现在放在下面,作为下层) */} + + + + + {/* 文案内容 */} + + + You Are Perfect.{"\n"} + Everything{"\n"} + Will Be Better. + {showConsent && ( <> - - - {t('consent.agree')} - + + @@ -90,74 +99,57 @@ export default function SplashScreen() { const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: '#FFF9F0', // 假设的背景色,根据图片调整 + backgroundColor: '#F5D3B5', // 匹配 Figma 背景色 alignItems: 'center', }, - image: { - width: width * 0.8, - height: height * 0.5, - marginTop: height * 0.1, + topImageContainer: { + marginTop: 60, + zIndex: 1, // 降低层级 + }, + topImage: { + width: 308, + height: 354, + }, + bgDecorationContainer: { + position: 'absolute', + left: -3, + zIndex: 2, // 提高层级,使其覆盖在图片之上 }, contentContainer: { alignItems: 'center', - marginTop: 20, - paddingHorizontal: 30, + zIndex: 3, }, - title: { - fontSize: 24, - fontWeight: 'bold', - color: '#4A3427', // 深褐色 - marginBottom: 10, + titleText: { + fontSize: 30, + lineHeight: 42, + color: '#772F00', // 匹配 Figma 文字颜色 textAlign: 'center', - fontFamily: Platform.OS === 'ios' ? 'Georgia' : 'serif', // 尝试匹配衬线体 - }, - subtitle: { - fontSize: 18, - fontWeight: 'bold', - color: '#4A3427', // 深褐色 - textAlign: 'center', - lineHeight: 24, - fontFamily: Platform.OS === 'ios' ? 'Georgia' : 'serif', + fontWeight: '600', + fontFamily: Platform.OS === 'ios' ? 'STIX Two Text' : 'serif', }, bottomContainer: { position: 'absolute', - bottom: 0, + bottom: 60, width: '100%', alignItems: 'center', - paddingBottom: 20, + zIndex: 4, }, - button: { - width: width * 0.8, - paddingVertical: 16, - borderRadius: 30, - alignItems: 'center', - justifyContent: 'center', - marginBottom: 20, - shadowColor: '#F69F7B', - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.3, - shadowRadius: 8, - elevation: 5, - }, - buttonText: { - color: '#FFF', - fontSize: 18, - fontWeight: '600', + buttonWrapper: { + marginBottom: 40, }, linksContainer: { flexDirection: 'row', alignItems: 'center', - marginBottom: 10, }, linkText: { fontSize: 12, - color: '#999', + color: 'rgba(119, 47, 0, 0.5)', // 使用半透明的文字颜色 textDecorationLine: 'underline', }, divider: { width: 1, height: 12, - backgroundColor: '#CCC', + backgroundColor: 'rgba(119, 47, 0, 0.2)', marginHorizontal: 15, }, }); diff --git a/client/app/_layout.tsx b/client/app/_layout.tsx index da817ae..b5f5188 100644 --- a/client/app/_layout.tsx +++ b/client/app/_layout.tsx @@ -3,12 +3,22 @@ import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native import { useFonts } from 'expo-font'; import { Stack } from 'expo-router'; import * as SplashScreen from 'expo-splash-screen'; +import * as Notifications from 'expo-notifications'; import { useEffect, useState } from 'react'; import 'react-native-reanimated'; import { useColorScheme } from '@/components/useColorScheme'; import { initI18n } from '@/src/i18n'; +// 配置通知处理方式(即使不发送也建议配置,以确保权限接口正常) +Notifications.setNotificationHandler({ + handleNotification: async () => ({ + shouldShowAlert: true, + shouldPlaySound: false, + shouldSetBadge: false, + }), +}); + export { // Catch any errors thrown by the Layout component. ErrorBoundary, diff --git a/client/app/index.tsx b/client/app/index.tsx index cf3d937..e9a5ae3 100644 --- a/client/app/index.tsx +++ b/client/app/index.tsx @@ -1,8 +1,9 @@ import { useEffect } from 'react'; import { ActivityIndicator, StyleSheet, View } from 'react-native'; import { useRouter } from 'expo-router'; +import AsyncStorage from '@react-native-async-storage/async-storage'; -import { getOnboardingCompleted, getConsentAccepted } from '@/src/storage/appStorage'; +import { getOnboardingCompleted, getConsentAccepted, setOnboardingCompleted, setConsentAccepted } from '@/src/storage/appStorage'; /** * 启动分发:根据 consent 和 onboarding 状态跳转 @@ -13,8 +14,9 @@ export default function Index() { useEffect(() => { let cancelled = false; (async () => { - // 临时重置引导页状态(开发调试用) - await setOnboardingCompleted(false); + // 【完全重置】:清除本地存储的所有数据(收藏、设置、引导状态等) + await AsyncStorage.clear(); + console.log('AsyncStorage has been cleared.'); // 1. 检查是否同意协议 const consentAccepted = await getConsentAccepted(); diff --git a/client/assets/images/home/Profile/Default_avatar.png b/client/assets/images/home/Profile/Default_avatar.png deleted file mode 100644 index d8aa749..0000000 Binary files a/client/assets/images/home/Profile/Default_avatar.png and /dev/null differ diff --git a/client/assets/images/home/Profile/Default_avatar.svg b/client/assets/images/home/Profile/Default_avatar.svg new file mode 100644 index 0000000..2870ae0 --- /dev/null +++ b/client/assets/images/home/Profile/Default_avatar.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/assets/images/home/Profile/widget/Widget1_en.png b/client/assets/images/home/Profile/widget/Widget1_en.png new file mode 100644 index 0000000..45e118a Binary files /dev/null and b/client/assets/images/home/Profile/widget/Widget1_en.png differ diff --git a/client/assets/images/home/Profile/widget/Widget1_tw.png b/client/assets/images/home/Profile/widget/Widget1_tw.png new file mode 100644 index 0000000..96b8e60 Binary files /dev/null and b/client/assets/images/home/Profile/widget/Widget1_tw.png differ diff --git a/client/assets/images/home/Profile/widget/Widget2_en.png b/client/assets/images/home/Profile/widget/Widget2_en.png new file mode 100644 index 0000000..c04db52 Binary files /dev/null and b/client/assets/images/home/Profile/widget/Widget2_en.png differ diff --git a/client/assets/images/home/Profile/widget/Widget2_tw.png b/client/assets/images/home/Profile/widget/Widget2_tw.png new file mode 100644 index 0000000..0cb5808 Binary files /dev/null and b/client/assets/images/home/Profile/widget/Widget2_tw.png differ diff --git a/client/assets/images/home/Profile/widget/Widget_description1_en.png b/client/assets/images/home/Profile/widget/Widget_description1_en.png new file mode 100644 index 0000000..7ee26ca Binary files /dev/null and b/client/assets/images/home/Profile/widget/Widget_description1_en.png differ diff --git a/client/assets/images/home/Profile/widget/Widget_description1_tw.png b/client/assets/images/home/Profile/widget/Widget_description1_tw.png new file mode 100644 index 0000000..acd6c21 Binary files /dev/null and b/client/assets/images/home/Profile/widget/Widget_description1_tw.png differ diff --git a/client/assets/images/home/Profile/widget/Widget_description2_en.png b/client/assets/images/home/Profile/widget/Widget_description2_en.png new file mode 100644 index 0000000..64a1625 Binary files /dev/null and b/client/assets/images/home/Profile/widget/Widget_description2_en.png differ diff --git a/client/assets/images/home/Profile/widget/Widget_description2_tw.png b/client/assets/images/home/Profile/widget/Widget_description2_tw.png new file mode 100644 index 0000000..31f48d7 Binary files /dev/null and b/client/assets/images/home/Profile/widget/Widget_description2_tw.png differ diff --git a/client/assets/images/home/Profile/widget/question_icon.svg b/client/assets/images/home/Profile/widget/question_icon.svg new file mode 100644 index 0000000..d262eb1 --- /dev/null +++ b/client/assets/images/home/Profile/widget/question_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/assets/images/icon/Push_icon.svg b/client/assets/images/icon/Push_icon.svg new file mode 100644 index 0000000..cd08c3f --- /dev/null +++ b/client/assets/images/icon/Push_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/assets/images/icon/Terms_icon.svg b/client/assets/images/icon/Terms_icon.svg new file mode 100644 index 0000000..3a7c24e --- /dev/null +++ b/client/assets/images/icon/Terms_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/assets/images/icon/add_icon.svg b/client/assets/images/icon/add_icon.svg new file mode 100644 index 0000000..0294ab7 --- /dev/null +++ b/client/assets/images/icon/add_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/assets/images/icon/back_icon.png b/client/assets/images/icon/back_icon.png new file mode 100644 index 0000000..06d5a4a Binary files /dev/null and b/client/assets/images/icon/back_icon.png differ diff --git a/client/assets/images/icon/btn_Notclicked.svg b/client/assets/images/icon/btn_Notclicked.svg new file mode 100644 index 0000000..a421323 --- /dev/null +++ b/client/assets/images/icon/btn_Notclicked.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/assets/images/icon/btn_clicked.svg b/client/assets/images/icon/btn_clicked.svg new file mode 100644 index 0000000..8d229e0 --- /dev/null +++ b/client/assets/images/icon/btn_clicked.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/client/assets/images/icon/enter_Light_icon.svg b/client/assets/images/icon/enter_Light_icon.svg new file mode 100644 index 0000000..fbdae4d --- /dev/null +++ b/client/assets/images/icon/enter_Light_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/assets/images/icon/language_icon.svg b/client/assets/images/icon/language_icon.svg new file mode 100644 index 0000000..3691630 --- /dev/null +++ b/client/assets/images/icon/language_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/assets/images/icon/like_icon.svg b/client/assets/images/icon/like_icon.svg new file mode 100644 index 0000000..a52fe32 --- /dev/null +++ b/client/assets/images/icon/like_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/assets/images/icon/mylike_icon.svg b/client/assets/images/icon/mylike_icon.svg new file mode 100644 index 0000000..59ef6ce --- /dev/null +++ b/client/assets/images/icon/mylike_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/assets/images/icon/next_icon.svg b/client/assets/images/icon/next_icon.svg new file mode 100644 index 0000000..863a77c --- /dev/null +++ b/client/assets/images/icon/next_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/assets/images/icon/privacy_icon.svg b/client/assets/images/icon/privacy_icon.svg new file mode 100644 index 0000000..08db4a1 --- /dev/null +++ b/client/assets/images/icon/privacy_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/assets/images/icon/reduce_icon.svg b/client/assets/images/icon/reduce_icon.svg new file mode 100644 index 0000000..3ac444d --- /dev/null +++ b/client/assets/images/icon/reduce_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/assets/images/icon/selected_icon.svg b/client/assets/images/icon/selected_icon.svg new file mode 100644 index 0000000..2a1607f --- /dev/null +++ b/client/assets/images/icon/selected_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/assets/images/icon/skip_icon.png b/client/assets/images/icon/skip_icon.png new file mode 100644 index 0000000..5379ca3 Binary files /dev/null and b/client/assets/images/icon/skip_icon.png differ diff --git a/client/assets/images/icon/weight_icon.svg b/client/assets/images/icon/weight_icon.svg new file mode 100644 index 0000000..e9184b5 --- /dev/null +++ b/client/assets/images/icon/weight_icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/client/assets/images/theme/theme_color.png b/client/assets/images/theme/theme_color.png new file mode 100644 index 0000000..7020149 Binary files /dev/null and b/client/assets/images/theme/theme_color.png differ diff --git a/client/assets/images/theme/theme_landscape.png b/client/assets/images/theme/theme_landscape.png new file mode 100644 index 0000000..f987049 Binary files /dev/null and b/client/assets/images/theme/theme_landscape.png differ diff --git a/client/babel.config.js b/client/babel.config.js deleted file mode 100644 index d413afc..0000000 --- a/client/babel.config.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = function (api) { - api.cache(true); - return { - presets: ['babel-preset-expo'], - plugins: [ - ['@babel/plugin-transform-react-jsx', { runtime: 'automatic' }] - ] - }; -}; diff --git a/client/components/home/.ProfileModal.tsx.swp b/client/components/home/.ProfileModal.tsx.swp new file mode 100644 index 0000000..9e07e56 Binary files /dev/null and b/client/components/home/.ProfileModal.tsx.swp differ diff --git a/client/components/home/ProfileModal.tsx b/client/components/home/ProfileModal.tsx index dc32fd6..e657e3f 100644 --- a/client/components/home/ProfileModal.tsx +++ b/client/components/home/ProfileModal.tsx @@ -1,5 +1,5 @@ -import React, { useEffect, useMemo, useState } from 'react'; -import { Alert, FlatList, Image, Pressable, StyleSheet, Text, View } from 'react-native'; +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'; @@ -11,6 +11,7 @@ import Animated, { SlideInRight, SlideOutLeft, SlideOutRight, + Layout, } from 'react-native-reanimated'; import SheetModal from '@/components/ui/SheetModal'; @@ -20,16 +21,25 @@ import { getDailyReminderSettings, getFavorites, setDailyReminderSettings, + removeFavorite, + getUserProfile, type DailyReminderSettings, + type FavoriteItem, } from '@/src/storage/appStorage'; -import AvatarIcon from '@/assets/images/home/Profile/Default_avatar.png'; -import MyLikeIcon from '@/assets/images/home/Profile/mylike.svg'; -import SmallComponentIcon from '@/assets/images/home/Profile/small_component.svg'; -import RemindIcon from '@/assets/images/home/Profile/remind.svg'; -import PrivacyIcon from '@/assets/images/home/Profile/privacy Policy.svg'; -import TermsIcon from '@/assets/images/home/Profile/terms_of_use.svg'; -import LanguageIcon from '@/assets/images/home/Profile/language.svg'; +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'; + +const { width } = Dimensions.get('window'); type Props = { visible: boolean; @@ -37,24 +47,38 @@ type Props = { onClose: () => void; }; -type Page = 'root' | 'favorites' | 'dailyReminder' | 'widget'; +type Page = 'root' | 'favorites' | 'dailyReminder' | 'widget' | 'language' | 'widgetHowTo'; type NavDirection = 'forward' | 'back'; -export default function ProfileModal({ visible, name, onClose }: Props) { +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 isRoot = page === 'root'; - // 关闭弹窗时重置为首页,避免下次打开停留在二级页 + // 当弹窗打开时,尝试从存储中获取最新的昵称,确保与 onboarding 同步 useEffect(() => { - if (!visible) { + if (visible) { + getUserProfile().then(profile => { + if (profile.name) { + setCurrentName(profile.name); + } + }); + } else { setNavDirection('back'); setPage('root'); } }, [visible]); + // 同步外部 propName 的变化 + useEffect(() => { + if (propName) { + setCurrentName(propName); + } + }, [propName]); + function go(next: Page, direction: NavDirection) { setNavDirection(direction); setPage(next); @@ -79,6 +103,8 @@ export default function ProfileModal({ visible, name, onClose }: Props) { 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]); @@ -107,42 +133,46 @@ export default function ProfileModal({ visible, name, onClose }: Props) { }, [navDirection]); return ( - - {!isRoot && ( - go('root', 'back')} - hitSlop={8} - accessibilityRole="button" - accessibilityLabel={t('common.back')} - style={styles.backRow} + + + - ‹ {t('common.back')} - - )} - - - + {page === 'root' ? ( go('favorites', 'forward')} onOpenWidget={() => go('widget', 'forward')} onOpenDailyReminder={() => go('dailyReminder', 'forward')} + onOpenLanguage={() => go('language', 'forward')} /> ) : page === 'favorites' ? ( - + ) : page === 'dailyReminder' ? ( go('root', 'back')} /> + ) : page === 'language' ? ( + + ) : page === 'widgetHowTo' ? ( + ) : ( - + go('widgetHowTo', 'forward')} /> )} + - + ); } @@ -156,17 +186,19 @@ function RootPage({ onOpenFavorites, onOpenWidget, onOpenDailyReminder, + onOpenLanguage, }: { name?: string; onOpenFavorites: () => void; onOpenWidget: () => void; onOpenDailyReminder: () => void; + onOpenLanguage: () => void; }) { const { t } = useTranslation(); return ( <> - + {name || 'Hali'} @@ -198,48 +230,79 @@ function RootPage({ } title={t('profile.language')} - onPress={() => toastTodo(t)} + onPress={onOpenLanguage} /> ); } -function FavoritesPage({ visible }: { visible: boolean }) { +function FavoritesPage({ visible, page }: { visible: boolean; page: Page }) { const { t } = useTranslation(); - const [ids, setIds] = useState([]); + const [favorites, setFavorites] = useState<(FavoriteItem & { text: string })[]>([]); - // 每次进入该页刷新一次,确保展示最新“喜欢” useEffect(() => { if (!visible) return; - let cancelled = false; - (async () => { - const list = await getFavorites(); - if (!cancelled) setIds(list); - })(); - return () => { - cancelled = true; - }; - }, [visible]); + refreshFavorites(); + }, [visible, page]); // 当页面切换回 favorites 时也刷新一次 - const items = useMemo(() => { - const map = new Map(MOCK_CONTENT.map((c) => [c.id, c])); - return ids.map((id) => map.get(id)).filter(Boolean) as { id: string; text: string }[]; - }, [ids]); + async function refreshFavorites() { + const storedFavs = await getFavorites(); + const map = new Map(MOCK_CONTENT.map((c) => [c.id, c.text])); + + const list = storedFavs + .map((fav) => ({ + ...fav, + text: map.get(fav.id) || '' + })) + .filter(item => item.text !== ''); + + setFavorites(list); + } + + async function handleRemove(id: string) { + // 1. 调用存储层移除收藏 + await removeFavorite(id); + // 2. 更新本地状态 + setFavorites(prev => prev.filter(item => item.id !== id)); + } return ( - {items.length === 0 ? ( + {favorites.length === 0 ? ( {t('favorites.empty')} ) : ( it.id} contentContainerStyle={styles.favList} + showsVerticalScrollIndicator={false} renderItem={({ item }) => ( - - {item.text} - + + + {item.date} + + + + {item.text} + handleRemove(item.id)} + style={styles.favRemoveBtn} + hitSlop={10} + > + + + + + )} /> )} @@ -252,16 +315,23 @@ function DailyReminderPage({ visible, onDone }: { visible: boolean; onDone: () = 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(); + + // 【测试模式】:强制模拟无权限状态 + const granted = false; + if (cancelled) return; setTimesPerDay(s.timesPerDay); - setPushEnabled(s.pushEnabled); + setPushEnabled(granted); + setHasSystemPermission(granted); setLoading(false); })(); return () => { @@ -269,6 +339,40 @@ function DailyReminderPage({ visible, onDone }: { visible: boolean; onDone: () = }; }, [visible]); + const handleTogglePush = async (value: boolean) => { + if (value) { + // 获取当前权限状态 + const settings = await Notifications.getPermissionsAsync(); + + // 如果已经拒绝,弹窗提示 + if (settings.status === 'denied') { + Alert.alert( + t('common.notice'), + "系统权限已被拒绝,请前往手机设置开启通知。" + ); + setPushEnabled(false); + return; + } + + // 尝试申请权限 + const { status } = await Notifications.requestPermissionsAsync(); + + // 调试:打印状态 + console.log('Push Permission Status:', status); + + if (status === 'granted') { + setPushEnabled(true); + setHasSystemPermission(true); + } else { + setPushEnabled(false); + setHasSystemPermission(false); + Alert.alert(t('common.notice'), t('dailyReminder.permissionDenied')); + } + } else { + setPushEnabled(false); + } + }; + function clamp(next: number) { return Math.min(10, Math.max(1, next)); } @@ -311,15 +415,24 @@ function DailyReminderPage({ visible, onDone }: { visible: boolean; onDone: () = - - - - + {!hasSystemPermission && ( + + + + + + {t('dailyReminder.pushLabel')} + + + - {t('dailyReminder.pushLabel')} - - + )} void }) { + const { t, i18n } = useTranslation(); + const currentLang = i18n.language; + + // 根据语言选择图片 + 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 ( - - - - - {t('widget.previewDate')} - - - 13:45 - - - {t('widget.lockScreen')} - + + + - - - - {t('widget.previewQuote')} - - - {t('widget.homeScreen')} - + + + + {t('widget.lockScreen')} + + + + + {t('widget.homeScreen')} + + + ); +} - {t('settings.widgetDesc')} +function WidgetHowToPage() { + const { t, i18n } = useTranslation(); + const currentLang = i18n.language; + const flatListRef = useRef(null); + const [activeIndex, setActiveIndex] = useState(0); + const [isManual, setIsManual] = useState(false); + const timerRef = useRef(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 { i18n } = useTranslation(); + const currentLang = i18n.language; + + const languages = [ + { id: 'zh-TW', label: '繁体' }, + { id: 'en', label: 'English' }, + ]; + + return ( + + + {languages.map((lang, index) => ( + changeLanguage(lang.id as any)} + > + {lang.label} + {currentLang === lang.id && ( + + )} + + ))} + ); } @@ -442,26 +671,31 @@ const styles = StyleSheet.create({ }, quickCard: { flex: 1, - borderRadius: 16, + borderRadius: 20, padding: 14, backgroundColor: 'rgba(244,214,194,0.65)', + height: 142, + justifyContent: 'space-between', }, quickTitle: { - color: '#5E2A28', - fontSize: 14, - fontWeight: '700', - marginBottom: 10, + color: '#482A0D', + fontSize: 15, + fontWeight: '500', + }, + quickIconWrap: { + alignItems: 'flex-end', + justifyContent: 'flex-end', + flex: 1 }, - quickIconWrap: { alignItems: 'center', justifyContent: 'center', flex: 1 }, list: { - borderRadius: 16, + borderRadius: 20, backgroundColor: 'rgba(255,255,255,0.75)', overflow: 'hidden', marginBottom: 8, }, item: { height: 52, - paddingHorizontal: 14, + paddingHorizontal: 18, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', @@ -471,55 +705,82 @@ const styles = StyleSheet.create({ itemLeft: { flexDirection: 'row', alignItems: 'center', - gap: 10, + gap: 12, }, itemIcon: { - width: 28, - height: 28, - borderRadius: 10, - backgroundColor: 'rgba(244,214,194,0.55)', + width: 24, + height: 24, alignItems: 'center', justifyContent: 'center', }, itemText: { - color: '#5E2A28', + color: '#522B09', fontSize: 15, - fontWeight: '600', + fontWeight: '500', }, chevron: { - color: 'rgba(94,42,40,0.45)', + color: '#C7B4A1', fontSize: 20, marginTop: -2, }, favContainer: { - borderRadius: 16, - backgroundColor: 'rgba(255,255,255,0.75)', - overflow: 'hidden', - marginBottom: 8, + flex: 1, + minHeight: 500, // 确保容器有足够高度 }, favEmpty: { color: 'rgba(94,42,40,0.55)', fontSize: 15, textAlign: 'center', - paddingVertical: 26, - paddingHorizontal: 14, + paddingVertical: 100, }, favList: { - paddingBottom: 10, + paddingHorizontal: 20, + paddingBottom: 80, }, - favRow: { - paddingHorizontal: 14, - paddingVertical: 14, - borderBottomWidth: StyleSheet.hairlineWidth, - borderBottomColor: 'rgba(94,42,40,0.10)', - backgroundColor: 'rgba(255,255,255,0.15)', + favCard: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: 24, }, - favText: { - color: '#5E2A28', + 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)', + }, + favThumbText: { fontSize: 15, lineHeight: 22, - fontWeight: '600', + color: '#5E2A28', + fontWeight: '500', + textAlign: 'center', + }, + favRemoveBtn: { + position: 'absolute', + top: 15, + right: 15, + width: 37, + height: 33, + alignItems: 'center', + justifyContent: 'center', }, counter: { @@ -563,15 +824,22 @@ const styles = StyleSheet.create({ }, remindRow: { height: 54, - borderRadius: 16, + 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, @@ -587,6 +855,7 @@ const styles = StyleSheet.create({ }, okPressable: { marginBottom: 6, + marginTop: 40, // 增加顶部间距以撑开高度 }, okBtn: { height: 52, @@ -602,61 +871,104 @@ const styles = StyleSheet.create({ }, widgetContent: { - paddingBottom: 18, - gap: 14, - }, - widgetRow: { - flexDirection: 'row', - gap: 12, - }, - widgetCard: { flex: 1, - borderRadius: 16, - overflow: 'hidden', - backgroundColor: 'rgba(255,255,255,0.75)', - borderWidth: StyleSheet.hairlineWidth, - borderColor: 'rgba(94,42,40,0.12)', + paddingTop: 10, }, - widgetPreviewInner: { - height: 120, - padding: 12, - justifyContent: 'center', + questionBtn: { + position: 'absolute', + right: 20, + top: -34, // 放在标题栏右侧 + zIndex: 10, }, - widgetPreviewLock: { - backgroundColor: 'rgba(244,214,194,0.65)', + widgetScroll: { + alignItems: 'center', + gap: 30, + paddingBottom: 40, }, - widgetPreviewHome: { - backgroundColor: 'rgba(243,208,225,0.55)', + widgetItem: { + alignItems: 'center', + width: '100%', }, - widgetPreviewDate: { - color: 'rgba(94,42,40,0.70)', - fontSize: 12, - fontWeight: '600', - marginBottom: 6, + widgetImg1: { + width: width * 0.9, + height: (width * 0.9) * (156 / 311), }, - widgetPreviewTime: { - color: '#ffffff', - fontSize: 44, - fontWeight: '800', - letterSpacing: 1, + widgetImg2: { + width: width * 0.9, + height: (width * 0.9) * (175 / 311), }, - widgetPreviewQuote: { - color: '#5E2A28', - fontSize: 14, - lineHeight: 20, - fontWeight: '700', + widgetLabel: { + marginTop: 12, + fontSize: 15, + color: 'rgba(94, 42, 40, 0.45)', + fontWeight: '500', }, - widgetCardLabel: { - paddingVertical: 10, + 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', - color: '#5E2A28', - fontSize: 13, - fontWeight: '700', + fontWeight: '500', + paddingHorizontal: 20, }, - widgetDesc: { - color: 'rgba(94,42,40,0.75)', - fontSize: 13, - lineHeight: 18, + 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', }, }); diff --git a/client/components/home/ThemeModal.tsx b/client/components/home/ThemeModal.tsx index d5f9b59..54e8207 100644 --- a/client/components/home/ThemeModal.tsx +++ b/client/components/home/ThemeModal.tsx @@ -16,7 +16,7 @@ type Props = { export default function ThemeModal({ visible, mode, onSelect, onClose }: Props) { const { t } = useTranslation(); return ( - + onSelect('scenery')} > @@ -35,7 +35,11 @@ export default function ThemeModal({ visible, mode, onSelect, onClose }: Props) selected={mode === 'color'} onPress={() => onSelect('color')} > - + @@ -56,11 +60,20 @@ function ThemeCard({ return ( - {children} - {title} + + + {children} + {/* 文案展示在图片中心 */} + + + {title} + + + + ); } @@ -68,43 +81,57 @@ function ThemeCard({ const styles = StyleSheet.create({ row: { flexDirection: 'row', - gap: 14, - paddingBottom: 18, + gap: 30, + paddingHorizontal: 10, + paddingBottom: 50, + paddingTop: 20, + justifyContent: 'center', }, - card: { - flex: 1, - borderRadius: 18, - padding: 10, - backgroundColor: 'rgba(255,255,255,0.55)', + cardContainer: { + alignItems: 'center', + width: 143, }, - cardSelected: { - borderWidth: 2, - borderColor: '#F99CC0', + previewWrapper: { + width: 138, + height: 203, + borderRadius: 26, + padding: 6.5, + justifyContent: 'center', + alignItems: 'center', }, - cardUnselected: { - borderWidth: StyleSheet.hairlineWidth, - borderColor: 'rgba(94,42,40,0.18)', + selectedWrapper: { + borderWidth: 4, + borderColor: '#E7837A', + borderRadius: 26, }, - preview: { - height: 96, - borderRadius: 14, + previewInner: { + width: '100%', + height: '100%', + borderRadius: 21, overflow: 'hidden', - backgroundColor: '#fff', - marginBottom: 10, + backgroundColor: '#F5F5F5', + position: 'relative', }, previewImage: { width: '100%', height: '100%', }, - colorPreview: { - flex: 1, - backgroundColor: '#F3D0E1', + textOverlay: { + ...StyleSheet.absoluteFillObject, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'rgba(0,0,0,0.05)', // 轻微遮罩增加文字可读性 }, - cardTitle: { - color: '#5E2A28', - fontSize: 14, + overlayTitle: { + color: '#FFFFFF', + fontSize: 15, fontWeight: '700', textAlign: 'center', + textShadowColor: 'rgba(0, 0, 0, 0.3)', + textShadowOffset: { width: 0, height: 1 }, + textShadowRadius: 3, + }, + selectedOverlayTitle: { + color: '#FFFFFF', }, }); - diff --git a/client/components/onboarding/NameInputStep.tsx b/client/components/onboarding/NameInputStep.tsx index beaddcb..1472d0f 100644 --- a/client/components/onboarding/NameInputStep.tsx +++ b/client/components/onboarding/NameInputStep.tsx @@ -1,32 +1,82 @@ -import React from 'react'; -import { View, StyleSheet, TextInput, Platform } from 'react-native'; -import { SerifText } from './SerifText'; +import React, { useEffect, useRef, useState } from 'react'; +import { View, StyleSheet, TextInput, Platform, Animated, TouchableOpacity, Dimensions, Text } from 'react-native'; import { OnboardingColors } from '@/constants/OnboardingTheme'; +import BtnNotClicked from '@/assets/images/icon/btn_Notclicked.svg'; +import BtnClicked from '@/assets/images/icon/btn_clicked.svg'; +import EnterLightIcon from '@/assets/images/icon/enter_Light_icon.svg'; + +const { height } = Dimensions.get('window'); interface NameInputStepProps { value: string; onChangeText: (text: string) => void; - onSubmitEditing?: () => void; + onNext: () => void; } -export function NameInputStep({ value, onChangeText, onSubmitEditing }: NameInputStepProps) { +export function NameInputStep({ value, onChangeText, onNext }: NameInputStepProps) { + const [isFocused, setIsFocused] = useState(false); + const blinkAnim = useRef(new Animated.Value(1)).current; + const hasInput = value.trim().length > 0; + + useEffect(() => { + const animation = Animated.loop( + Animated.sequence([ + Animated.timing(blinkAnim, { toValue: 0, duration: 500, useNativeDriver: true }), + Animated.timing(blinkAnim, { toValue: 1, duration: 500, useNativeDriver: true }), + ]) + ); + if (isFocused) { + animation.start(); + } else { + animation.stop(); + blinkAnim.setValue(0); + } + return () => animation.stop(); + }, [blinkAnim, isFocused]); + return ( - 我可以怎么称呼你? - - - + + + {/* 显示层:文案 + 跟随的光标 */} + + + {hasInput ? value : (isFocused ? "" : "Mama")} + + {isFocused && ( + + + + )} + + + {/* 交互层:隐藏的输入框 */} + setIsFocused(true)} + onBlur={() => setIsFocused(false)} + caretHidden={true} + autoCorrect={false} + spellCheck={false} + /> + + + + + + {hasInput ? : } + ); @@ -34,31 +84,53 @@ export function NameInputStep({ value, onChangeText, onSubmitEditing }: NameInpu const styles = StyleSheet.create({ container: { + flex: 1, alignItems: 'center', - width: '100%', + paddingTop: 20, }, - title: { - fontSize: 24, - marginBottom: 40, - textAlign: 'center', - }, - inputContainer: { - width: '100%', + inputCard: { + width: 335, + height: 75, backgroundColor: OnboardingColors.cardBackground, borderRadius: 20, - paddingVertical: 20, - paddingHorizontal: 24, + justifyContent: 'center', + alignItems: 'center', shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.05, shadowRadius: 10, elevation: 2, }, - input: { - fontSize: 24, - fontFamily: Platform.select({ ios: 'Georgia', android: 'serif', default: 'serif' }), + inputWrapper: { + width: '100%', + height: '100%', + justifyContent: 'center', + alignItems: 'center', + }, + displayLayer: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + }, + displayText: { + fontSize: 22, + fontFamily: Platform.select({ ios: 'STIX Two Text', android: 'serif', default: 'serif' }), + fontWeight: '600', color: OnboardingColors.textPrimary, textAlign: 'center', - padding: 0, // remove default padding }, + hiddenInput: { + ...StyleSheet.absoluteFillObject, + color: 'transparent', // 文字透明,只负责输入逻辑 + fontSize: 22, + textAlign: 'center', + }, + cursorWrapper: { + // 默认居中显示时,光标在左侧 + }, + footer: { + position: 'absolute', + bottom: height * 0.12, + alignItems: 'center', + } }); diff --git a/client/components/onboarding/OnboardingLayout.tsx b/client/components/onboarding/OnboardingLayout.tsx index 33a014e..9644db7 100644 --- a/client/components/onboarding/OnboardingLayout.tsx +++ b/client/components/onboarding/OnboardingLayout.tsx @@ -1,45 +1,61 @@ import React from 'react'; -import { View, StyleSheet, SafeAreaView, TouchableOpacity, StatusBar } from 'react-native'; +import { View, StyleSheet, SafeAreaView, TouchableOpacity, StatusBar, Text, Image, Platform } from 'react-native'; import { OnboardingColors } from '@/constants/OnboardingTheme'; -import { SerifText } from './SerifText'; -import { NextButton } from './NextButton'; interface OnboardingLayoutProps { children: React.ReactNode; - onSkip?: () => void; - onNext?: () => void; - nextEnabled?: boolean; - showNextButton?: boolean; + title?: string; + currentStep: number; + totalSteps: number; + onSkip: () => void; + onBack?: () => void; + showBackButton?: boolean; } export function OnboardingLayout({ children, + title, + currentStep, + totalSteps, onSkip, - onNext, - nextEnabled = true, - showNextButton = true + onBack, + showBackButton = false }: OnboardingLayoutProps) { return ( + {/* Header: Back & Skip */} - - {onSkip && ( - - Skip {'->'} - - )} - - - - {children} + + {showBackButton && onBack && ( + + + + )} + + + + skip + + - - {showNextButton && onNext && ( - - )} + {/* Title & Progress Row */} + + {title} + ({currentStep}/{totalSteps}) + + + {/* Content */} + + {children} @@ -58,27 +74,60 @@ const styles = StyleSheet.create({ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', - paddingHorizontal: 24, - paddingVertical: 12, + paddingHorizontal: 20, + height: 44, }, - spacer: { - width: 60, // Balance the skip button width approximately + headerLeft: { + width: 44, + height: 44, + justifyContent: 'center', + }, + iconButton: { + padding: 8, + }, + backIcon: { + width: 19, + height: 19, + resizeMode: 'contain', }, skipButton: { + flexDirection: 'row', + alignItems: 'center', padding: 8, }, skipText: { - fontSize: 16, - color: OnboardingColors.textPrimary, + fontSize: 13, + color: OnboardingColors.textMuted, + fontFamily: Platform.OS === 'ios' ? 'STIX Two Text' : 'serif', + marginRight: 4, + }, + skipIcon: { + width: 10, + height: 4, + resizeMode: 'contain', + }, + titleRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-end', + paddingHorizontal: 20, + marginTop: 20, + marginBottom: 20, + }, + questionTitle: { + fontSize: 22, + color: OnboardingColors.questionTitle, + fontFamily: Platform.OS === 'ios' ? 'PingFang TC' : 'sans-serif', + flex: 1, + }, + progressText: { + fontSize: 18, + color: OnboardingColors.textProgress, + fontFamily: Platform.OS === 'ios' ? 'PingFang TC' : 'sans-serif', + marginLeft: 10, }, content: { flex: 1, - justifyContent: 'center', - paddingHorizontal: 24, - }, - footer: { - alignItems: 'center', - paddingBottom: 40, - minHeight: 100, // Reserve space for button + paddingHorizontal: 20, }, }); diff --git a/client/components/onboarding/ReminderStep.tsx b/client/components/onboarding/ReminderStep.tsx new file mode 100644 index 0000000..165fe8d --- /dev/null +++ b/client/components/onboarding/ReminderStep.tsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { View, StyleSheet, TouchableOpacity, Text, Platform, Dimensions } from 'react-native'; +import { OnboardingColors } from '@/constants/OnboardingTheme'; +import AddIcon from '@/assets/images/icon/add_icon.svg'; +import ReduceIcon from '@/assets/images/icon/reduce_icon.svg'; +import BtnClicked from '@/assets/images/icon/btn_clicked.svg'; + +const { height } = Dimensions.get('window'); + +interface ReminderStepProps { + value: number; + onChange: (value: number) => void; + onFinish: () => void; +} + +export function ReminderStep({ value, onChange, onFinish }: ReminderStepProps) { + const handleReduce = () => { + if (value > 1) onChange(value - 1); + }; + + const handleAdd = () => { + if (value < 5) onChange(value + 1); + }; + + return ( + + + + + + + + {value} + + + + + + + + + + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'center', + paddingTop: 20, + }, + counterContainer: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + width: '100%', + marginTop: 40, + }, + numberWrapper: { + flexDirection: 'row', + alignItems: 'flex-end', + marginHorizontal: 40, + }, + numberText: { + fontSize: 107, + fontFamily: Platform.OS === 'ios' ? 'STIX Two Text' : 'serif', + fontWeight: '600', + color: OnboardingColors.textPrimary, + lineHeight: 120, + }, + unitText: { + fontSize: 17, + fontFamily: Platform.OS === 'ios' ? 'STIX Two Text' : 'serif', + fontWeight: '600', + color: OnboardingColors.textPrimary, + marginBottom: 20, + marginLeft: 4, + }, + footer: { + position: 'absolute', + bottom: height * 0.12, + alignItems: 'center', + } +}); diff --git a/client/components/onboarding/SelectionStep.tsx b/client/components/onboarding/SelectionStep.tsx new file mode 100644 index 0000000..8268cca --- /dev/null +++ b/client/components/onboarding/SelectionStep.tsx @@ -0,0 +1,103 @@ +import React from 'react'; +import { View, StyleSheet, TouchableOpacity, ScrollView, Dimensions } from 'react-native'; +import { OnboardingColors } from '@/constants/OnboardingTheme'; +import { SerifText } from './SerifText'; +import SelectedIcon from '@/assets/images/icon/selected_icon.svg'; +import BtnNotClicked from '@/assets/images/icon/btn_Notclicked.svg'; +import BtnClicked from '@/assets/images/icon/btn_clicked.svg'; + +const { height } = Dimensions.get('window'); + +interface Option { + id: string; + label: string; +} + +interface SelectionStepProps { + options: Option[]; + selectedIds: string[]; + onToggle: (id: string) => void; + onNext: () => void; +} + +export function SelectionStep({ options, selectedIds, onToggle, onNext }: SelectionStepProps) { + const hasSelection = selectedIds.length > 0; + + return ( + + + {options.map((option) => { + const isSelected = selectedIds.includes(option.id); + return ( + onToggle(option.id)} + activeOpacity={0.7} + > + {option.label} + {isSelected && ( + + + + )} + + ); + })} + + + {/* 底部按钮:距离底部 12% 高度 */} + + + {hasSelection ? : } + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + paddingTop: 20, + }, + optionsList: { + paddingBottom: 150, // 为底部按钮留出空间 + }, + optionCard: { + width: '100%', + height: 75, + backgroundColor: OnboardingColors.cardBackground, + borderRadius: 20, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 24, + marginBottom: 12, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.05, + shadowRadius: 10, + elevation: 2, + }, + optionText: { + fontSize: 18, + color: OnboardingColors.textPrimary, + fontWeight: '500', + flex: 1, + }, + iconWrapper: { + marginLeft: 10, + }, + footer: { + position: 'absolute', + bottom: height * 0.12, + left: 0, + right: 0, + alignItems: 'center', + } +}); diff --git a/client/components/ui/.SheetModal.tsx.swp b/client/components/ui/.SheetModal.tsx.swp new file mode 100644 index 0000000..e0c938d Binary files /dev/null and b/client/components/ui/.SheetModal.tsx.swp differ diff --git a/client/components/ui/SheetModal.tsx b/client/components/ui/SheetModal.tsx index e0fc3ea..bdb9d97 100644 --- a/client/components/ui/SheetModal.tsx +++ b/client/components/ui/SheetModal.tsx @@ -1,5 +1,5 @@ -import React, { useEffect, useMemo, useState } from 'react'; -import { Modal, Pressable, StyleSheet, Text, View } from 'react-native'; +import React, { useEffect, useMemo, useState, useRef } from 'react'; +import { Modal, Pressable, StyleSheet, Text, View, PanResponder, Animated as RNAnimated, Dimensions, Image, ImageSourcePropType } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import Animated, { Easing, @@ -9,26 +9,35 @@ import Animated, { withTiming, } from 'react-native-reanimated'; +const { height: SCREEN_HEIGHT } = Dimensions.get('window'); +const FIXED_TOP_GAP = 100; // 统一距离顶部的高度 + type Props = { visible: boolean; title?: string; onClose: () => void; children: React.ReactNode; + leftIcon?: ImageSourcePropType; // 新增:支持自定义左侧图标 + height?: number; // 新增:支持自定义高度 }; /** * 通用底部上拉弹窗(Sheet) * - 内容区背景色固定:#FAF3EC - * - 关闭方式:点 X(本期不要求点遮罩关闭) + * - 高度固定:默认距离顶部固定间距,也支持传入指定高度 */ -export default function SheetModal({ visible, title, onClose, children }: Props) { +export default function SheetModal({ visible, title, onClose, children, leftIcon, height: customHeight }: Props) { const insets = useSafeAreaInsets(); const [mounted, setMounted] = useState(false); const progress = useSharedValue(0); // 0: 关闭, 1: 打开 + const dragY = useSharedValue(0); // 拖拽位移 + + const sheetHeight = customHeight || (SCREEN_HEIGHT - FIXED_TOP_GAP); useEffect(() => { if (visible) { setMounted(true); + dragY.value = 0; progress.value = withTiming(1, { duration: 260, easing: Easing.out(Easing.cubic) }); return; } @@ -40,46 +49,88 @@ export default function SheetModal({ visible, title, onClose, children }: Props) if (finished) runOnJS(setMounted)(false); } ); - }, [visible, mounted, progress]); + }, [visible, mounted, progress, dragY]); + + // 使用 PanResponder 处理下滑手势 + const panResponder = useRef( + PanResponder.create({ + onStartShouldSetPanResponder: () => false, + onMoveShouldSetPanResponder: () => false, // 暂时屏蔽下拉关闭手势,解决滑动冲突 + onPanResponderMove: (_, gestureState) => { + if (gestureState.dy > 0) { + dragY.value = gestureState.dy; + } + }, + onPanResponderRelease: (_, gestureState) => { + if (gestureState.dy > 80 || gestureState.vy > 0.5) { + runOnJS(onClose)(); + } else { + dragY.value = withTiming(0, { + duration: 300, + easing: Easing.out(Easing.back(1)) + }); + } + }, + onPanResponderTerminate: () => { + dragY.value = withTiming(0, { duration: 200 }); + }, + }) + ).current; const overlayStyle = useAnimatedStyle(() => { return { opacity: 0.4 * progress.value }; }); const sheetStyle = useAnimatedStyle(() => { - const translateY = (1 - progress.value) * 380; - return { transform: [{ translateY }] }; + const baseTranslateY = (1 - progress.value) * sheetHeight; // 基础位移 + return { + transform: [{ translateY: baseTranslateY + dragY.value }] + }; }); - const containerPaddingBottom = useMemo(() => Math.max(insets.bottom, 12), [insets.bottom]); + const containerPaddingBottom = useMemo(() => Math.max(insets.bottom, 80), [insets.bottom]); // 增加底部间距至 80,约占 350 高度的 22%,确保内容不被截断并留出足够呼吸感 // 注意:Modal 的 visible 必须为 true 才会渲染,因此用 mounted 保持退场动画 return ( - + + + + + + {title ?? ''} - × + {leftIcon ? ( + + ) : ( + × + )} @@ -103,9 +154,19 @@ const styles = StyleSheet.create({ backgroundColor: '#FAF3EC', borderTopLeftRadius: 24, borderTopRightRadius: 24, - paddingTop: 14, + paddingTop: 8, paddingHorizontal: 16, }, + handleContainer: { + alignItems: 'center', + paddingVertical: 8, + }, + handle: { + width: 40, + height: 5, + borderRadius: 2.5, + backgroundColor: 'rgba(94,42,40,0.15)', + }, header: { height: 44, justifyContent: 'center', @@ -131,8 +192,13 @@ const styles = StyleSheet.create({ lineHeight: 28, fontWeight: '400', }, + backIcon: { + width: 20, + height: 20, + resizeMode: 'contain', + }, body: { paddingTop: 10, + flex: 1, }, }); - diff --git a/client/constants/OnboardingTheme.ts b/client/constants/OnboardingTheme.ts index 7d8354a..871489f 100644 --- a/client/constants/OnboardingTheme.ts +++ b/client/constants/OnboardingTheme.ts @@ -1,9 +1,14 @@ export const OnboardingColors = { background: '#FFF4EA', - textPrimary: '#4A3B32', - textSecondary: '#8C8C8C', + textPrimary: '#772F00', + textSecondary: '#DED2CA', // 默认 Mama 字体颜色 + textMuted: '#A27854', // Skip 按钮颜色 + textProgress: '#D4B08E', // (0/5) 颜色 + questionTitle: '#B8504D', // 问题标题颜色 + buttonNotClicked: '#F2DDCA', buttonStart: '#F69F7B', buttonEnd: '#F99CC0', cardBackground: '#FFFFFF', - cursor: '#F99CC0', + cardSelected: '#FFD8E2', // 选中后的背景色 + cursor: '#F89DB4', }; diff --git a/client/ios/Podfile.lock b/client/ios/Podfile.lock index a555c69..87fc2f5 100644 --- a/client/ios/Podfile.lock +++ b/client/ios/Podfile.lock @@ -3,6 +3,9 @@ PODS: - ExpoModulesCore - EXConstants (18.0.13): - ExpoModulesCore + - EXJSONUtils (0.15.0) + - EXManifests (1.0.10): + - ExpoModulesCore - EXNotifications (0.32.16): - ExpoModulesCore - Expo (54.0.32): @@ -30,6 +33,177 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga + - expo-dev-client (6.0.20): + - EXManifests + - expo-dev-launcher + - expo-dev-menu + - expo-dev-menu-interface + - EXUpdatesInterface + - expo-dev-launcher (6.0.20): + - EXManifests + - expo-dev-launcher/Main (= 6.0.20) + - expo-dev-menu + - expo-dev-menu-interface + - ExpoModulesCore + - EXUpdatesInterface + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-jsinspector + - React-NativeModulesApple + - React-RCTAppDelegate + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactAppDependencyProvider + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - expo-dev-launcher/Main (6.0.20): + - EXManifests + - expo-dev-launcher/Unsafe + - expo-dev-menu + - expo-dev-menu-interface + - ExpoModulesCore + - EXUpdatesInterface + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-jsinspector + - React-NativeModulesApple + - React-RCTAppDelegate + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactAppDependencyProvider + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - expo-dev-launcher/Unsafe (6.0.20): + - EXManifests + - expo-dev-menu + - expo-dev-menu-interface + - ExpoModulesCore + - EXUpdatesInterface + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-jsinspector + - React-NativeModulesApple + - React-RCTAppDelegate + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactAppDependencyProvider + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - expo-dev-menu (7.0.18): + - expo-dev-menu/Main (= 7.0.18) + - expo-dev-menu/ReactNativeCompatibles (= 7.0.18) + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - expo-dev-menu-interface (2.0.0) + - expo-dev-menu/Main (7.0.18): + - EXManifests + - expo-dev-menu-interface + - ExpoModulesCore + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-jsinspector + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - expo-dev-menu/ReactNativeCompatibles (7.0.18): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga - ExpoAsset (12.0.12): - ExpoModulesCore - ExpoFileSystem (19.0.21): @@ -74,6 +248,8 @@ PODS: - ExpoModulesCore - ExpoWebBrowser (15.0.10): - ExpoModulesCore + - EXUpdatesInterface (2.0.0): + - ExpoModulesCore - FBLazyVector (0.81.5) - hermes-engine (0.81.5): - hermes-engine/Pre-built (= 0.81.5) @@ -1798,6 +1974,28 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga + - RNGestureHandler (2.30.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga - RNReanimated (4.1.6): - hermes-engine - RCTRequired @@ -2040,12 +2238,18 @@ PODS: DEPENDENCIES: - "EXApplication (from `../node_modules/.pnpm/expo-application@7.0.8_expo@54.0.32/node_modules/expo-application/ios`)" - "EXConstants (from `../node_modules/.pnpm/expo-constants@18.0.13_expo@54.0.32_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0_/node_modules/expo-constants/ios`)" + - "EXJSONUtils (from `../node_modules/.pnpm/expo-json-utils@0.15.0/node_modules/expo-json-utils/ios`)" + - "EXManifests (from `../node_modules/.pnpm/expo-manifests@1.0.10_expo@54.0.32/node_modules/expo-manifests/ios`)" - "EXNotifications (from `../node_modules/.pnpm/expo-notifications@0.32.16_expo@54.0.32_react-native@0.81.5_@babel+core@7.28.6_@types+r_758952db70529f49bda448def1c13c49/node_modules/expo-notifications/ios`)" - "Expo (from `../node_modules/.pnpm/expo@54.0.32_@babel+core@7.28.6_@expo+metro-runtime@6.1.2_expo-router@6.0.22_react-nati_18ad48ba284ee86e6eb1cb0f939697b0/node_modules/expo`)" + - "expo-dev-client (from `../node_modules/.pnpm/expo-dev-client@6.0.20_expo@54.0.32/node_modules/expo-dev-client/ios`)" + - "expo-dev-launcher (from `../node_modules/.pnpm/expo-dev-launcher@6.0.20_expo@54.0.32/node_modules/expo-dev-launcher`)" + - "expo-dev-menu (from `../node_modules/.pnpm/expo-dev-menu@7.0.18_expo@54.0.32/node_modules/expo-dev-menu`)" + - "expo-dev-menu-interface (from `../node_modules/.pnpm/expo-dev-menu-interface@2.0.0_expo@54.0.32/node_modules/expo-dev-menu-interface/ios`)" - "ExpoAsset (from `../node_modules/.pnpm/expo-asset@12.0.12_expo@54.0.32_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0__react@19.1.0/node_modules/expo-asset/ios`)" - "ExpoFileSystem (from `../node_modules/.pnpm/expo-file-system@19.0.21_expo@54.0.32_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0_/node_modules/expo-file-system/ios`)" - "ExpoFont (from `../node_modules/.pnpm/expo-font@14.0.11_expo@54.0.32_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0__react@19.1.0/node_modules/expo-font/ios`)" - - "ExpoHead (from `../node_modules/.pnpm/expo-router@6.0.22_@expo+metro-runtime@6.1.2_@types+react@19.1.17_expo-constants@18.0.1_4eba8c13adbbc1edf57cc04c4adac5f6/node_modules/expo-router/ios`)" + - "ExpoHead (from `../node_modules/.pnpm/expo-router@6.0.22_@expo+metro-runtime@6.1.2_@types+react@19.1.17_expo-constants@18.0.1_bd9aa16746ed7110429f931eb008e6d2/node_modules/expo-router/ios`)" - "ExpoKeepAwake (from `../node_modules/.pnpm/expo-keep-awake@15.0.8_expo@54.0.32_react@19.1.0/node_modules/expo-keep-awake/ios`)" - "ExpoLinearGradient (from `../node_modules/.pnpm/expo-linear-gradient@15.0.8_expo@54.0.32_react-native@0.81.5_@babel+core@7.28.6_@types+_53aef72480df9baa4504f4743d9c64bb/node_modules/expo-linear-gradient/ios`)" - "ExpoLinking (from `../node_modules/.pnpm/expo-linking@8.0.11_expo@54.0.32_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0__react@19.1.0/node_modules/expo-linking/ios`)" @@ -2053,6 +2257,7 @@ DEPENDENCIES: - "ExpoModulesCore (from `../node_modules/.pnpm/expo-modules-core@3.0.29_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0__react@19.1.0/node_modules/expo-modules-core`)" - "ExpoSplashScreen (from `../node_modules/.pnpm/expo-splash-screen@31.0.13_expo@54.0.32/node_modules/expo-splash-screen/ios`)" - "ExpoWebBrowser (from `../node_modules/.pnpm/expo-web-browser@15.0.10_expo@54.0.32_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0_/node_modules/expo-web-browser/ios`)" + - "EXUpdatesInterface (from `../node_modules/.pnpm/expo-updates-interface@2.0.0_expo@54.0.32/node_modules/expo-updates-interface/ios`)" - "FBLazyVector (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/Libraries/FBLazyVector`)" - "hermes-engine (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)" - "RCTDeprecation (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`)" @@ -2122,6 +2327,7 @@ DEPENDENCIES: - "ReactCommon/turbomodule/core (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/ReactCommon`)" - "ReactNativeDependencies (from `../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/third-party-podspecs/ReactNativeDependencies.podspec`)" - "RNCAsyncStorage (from `../node_modules/.pnpm/@react-native-async-storage+async-storage@2.2.0_react-native@0.81.5_@babel+core@7.28.6__ce3c4972004f3d6791573ec5b64bee38/node_modules/@react-native-async-storage/async-storage`)" + - "RNGestureHandler (from `../node_modules/.pnpm/react-native-gesture-handler@2.30.0_react-native@0.81.5_@babel+core@7.28.6_@types+react_39cf7da47c9c8531caaa923ee740e293/node_modules/react-native-gesture-handler`)" - "RNReanimated (from `../node_modules/.pnpm/react-native-reanimated@4.1.6_@babel+core@7.28.6_react-native-worklets@0.5.1_@babel+cor_c7c888bd389fb93c9cfe2d3c1c8b0777/node_modules/react-native-reanimated`)" - "RNScreens (from `../node_modules/.pnpm/react-native-screens@4.16.0_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0__react@19.1.0/node_modules/react-native-screens`)" - "RNSVG (from `../node_modules/.pnpm/react-native-svg@15.12.1_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0__react@19.1.0/node_modules/react-native-svg`)" @@ -2133,10 +2339,22 @@ EXTERNAL SOURCES: :path: "../node_modules/.pnpm/expo-application@7.0.8_expo@54.0.32/node_modules/expo-application/ios" EXConstants: :path: "../node_modules/.pnpm/expo-constants@18.0.13_expo@54.0.32_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0_/node_modules/expo-constants/ios" + EXJSONUtils: + :path: "../node_modules/.pnpm/expo-json-utils@0.15.0/node_modules/expo-json-utils/ios" + EXManifests: + :path: "../node_modules/.pnpm/expo-manifests@1.0.10_expo@54.0.32/node_modules/expo-manifests/ios" EXNotifications: :path: "../node_modules/.pnpm/expo-notifications@0.32.16_expo@54.0.32_react-native@0.81.5_@babel+core@7.28.6_@types+r_758952db70529f49bda448def1c13c49/node_modules/expo-notifications/ios" Expo: :path: "../node_modules/.pnpm/expo@54.0.32_@babel+core@7.28.6_@expo+metro-runtime@6.1.2_expo-router@6.0.22_react-nati_18ad48ba284ee86e6eb1cb0f939697b0/node_modules/expo" + expo-dev-client: + :path: "../node_modules/.pnpm/expo-dev-client@6.0.20_expo@54.0.32/node_modules/expo-dev-client/ios" + expo-dev-launcher: + :path: "../node_modules/.pnpm/expo-dev-launcher@6.0.20_expo@54.0.32/node_modules/expo-dev-launcher" + expo-dev-menu: + :path: "../node_modules/.pnpm/expo-dev-menu@7.0.18_expo@54.0.32/node_modules/expo-dev-menu" + expo-dev-menu-interface: + :path: "../node_modules/.pnpm/expo-dev-menu-interface@2.0.0_expo@54.0.32/node_modules/expo-dev-menu-interface/ios" ExpoAsset: :path: "../node_modules/.pnpm/expo-asset@12.0.12_expo@54.0.32_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0__react@19.1.0/node_modules/expo-asset/ios" ExpoFileSystem: @@ -2144,7 +2362,7 @@ EXTERNAL SOURCES: ExpoFont: :path: "../node_modules/.pnpm/expo-font@14.0.11_expo@54.0.32_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0__react@19.1.0/node_modules/expo-font/ios" ExpoHead: - :path: "../node_modules/.pnpm/expo-router@6.0.22_@expo+metro-runtime@6.1.2_@types+react@19.1.17_expo-constants@18.0.1_4eba8c13adbbc1edf57cc04c4adac5f6/node_modules/expo-router/ios" + :path: "../node_modules/.pnpm/expo-router@6.0.22_@expo+metro-runtime@6.1.2_@types+react@19.1.17_expo-constants@18.0.1_bd9aa16746ed7110429f931eb008e6d2/node_modules/expo-router/ios" ExpoKeepAwake: :path: "../node_modules/.pnpm/expo-keep-awake@15.0.8_expo@54.0.32_react@19.1.0/node_modules/expo-keep-awake/ios" ExpoLinearGradient: @@ -2159,6 +2377,8 @@ EXTERNAL SOURCES: :path: "../node_modules/.pnpm/expo-splash-screen@31.0.13_expo@54.0.32/node_modules/expo-splash-screen/ios" ExpoWebBrowser: :path: "../node_modules/.pnpm/expo-web-browser@15.0.10_expo@54.0.32_react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0_/node_modules/expo-web-browser/ios" + EXUpdatesInterface: + :path: "../node_modules/.pnpm/expo-updates-interface@2.0.0_expo@54.0.32/node_modules/expo-updates-interface/ios" FBLazyVector: :path: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/Libraries/FBLazyVector" hermes-engine: @@ -2296,6 +2516,8 @@ EXTERNAL SOURCES: :podspec: "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.28.6_@types+react@19.1.17_react@19.1.0/node_modules/react-native/third-party-podspecs/ReactNativeDependencies.podspec" RNCAsyncStorage: :path: "../node_modules/.pnpm/@react-native-async-storage+async-storage@2.2.0_react-native@0.81.5_@babel+core@7.28.6__ce3c4972004f3d6791573ec5b64bee38/node_modules/@react-native-async-storage/async-storage" + RNGestureHandler: + :path: "../node_modules/.pnpm/react-native-gesture-handler@2.30.0_react-native@0.81.5_@babel+core@7.28.6_@types+react_39cf7da47c9c8531caaa923ee740e293/node_modules/react-native-gesture-handler" RNReanimated: :path: "../node_modules/.pnpm/react-native-reanimated@4.1.6_@babel+core@7.28.6_react-native-worklets@0.5.1_@babel+cor_c7c888bd389fb93c9cfe2d3c1c8b0777/node_modules/react-native-reanimated" RNScreens: @@ -2310,8 +2532,14 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: EXApplication: 1e98d4b1dccdf30627f92917f4b2c5a53c330e5f EXConstants: fce59a631a06c4151602843667f7cfe35f81e271 + EXJSONUtils: 1d3e4590438c3ee593684186007028a14b3686cd + EXManifests: a8d97683e5c7a3b026ffbd58559c64dc655b747b EXNotifications: 9eec98712cc814ceff916d876cb53859003b0597 Expo: 4e503a041c59c4e34c8be262a135848ad5cd3710 + expo-dev-client: 425ee077d6754a98cfe3a2e2410d29b440b24c9d + expo-dev-launcher: a4f4cdef064ab1fb8621e5b8c7c457cd6e9568c3 + expo-dev-menu: 05b18812110c175814c6af0d09dd658abcc5e00d + expo-dev-menu-interface: 600df12ea01efecdd822daaf13cc0ac091775533 ExpoAsset: f867e55ceb428aab99e1e8c082b5aee7c159ea18 ExpoFileSystem: 858a44267a3e6e9057e0888ad7c7cfbf55d52063 ExpoFont: f543ce20a228dd702813668b1a07b46f51878d47 @@ -2323,6 +2551,7 @@ SPEC CHECKSUMS: ExpoModulesCore: f3da4f1ab5a8375d0beafab763739dbee8446583 ExpoSplashScreen: bc3cffefca2716e5f22350ca109badd7e50ec14d ExpoWebBrowser: 17b064c621789e41d4816c95c93f429b84971f52 + EXUpdatesInterface: 5adf50cb41e079c861da6d9b4b954c3db9a50734 FBLazyVector: e95a291ad2dadb88e42b06e0c5fb8262de53ec12 hermes-engine: 9f4dfe93326146a1c99eb535b1cb0b857a3cd172 RCTDeprecation: 943572d4be82d480a48f4884f670135ae30bf990 @@ -2391,6 +2620,7 @@ SPEC CHECKSUMS: ReactCommon: 08810150b1206cc44aecf5f6ae19af32f29151a8 ReactNativeDependencies: 71ce9c28beb282aa720ea7b46980fff9669f428a RNCAsyncStorage: 3a4f5e2777dae1688b781a487923a08569e27fe4 + RNGestureHandler: e0d0bce5599f6120b7adf90c38d2805e2935795f RNReanimated: 9c6a550b41de91cf374e60afd79db93a362f1126 RNScreens: d8d6f1792f6e7ac12b0190d33d8d390efc0c1845 RNSVG: 31d6639663c249b7d5abc9728dde2041eb2a3c34 diff --git a/client/ios/client.xcodeproj/project.pbxproj b/client/ios/client.xcodeproj/project.pbxproj index 4a11c49..37c23a4 100644 --- a/client/ios/client.xcodeproj/project.pbxproj +++ b/client/ios/client.xcodeproj/project.pbxproj @@ -374,6 +374,8 @@ "${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/expo-dev-launcher/EXDevLauncher.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/expo-dev-menu/EXDevMenu.bundle", ); name = "[CP] Copy Pods Resources"; outputPaths = ( @@ -387,6 +389,8 @@ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXDevLauncher.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXDevMenu.bundle", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; diff --git a/client/ios/情绪小组件/EmotionWidget.swift b/client/ios/情绪小组件/EmotionWidget.swift index 3376cfa..510cc41 100644 --- a/client/ios/情绪小组件/EmotionWidget.swift +++ b/client/ios/情绪小组件/EmotionWidget.swift @@ -1,28 +1,54 @@ import WidgetKit import SwiftUI -// V1:写死文案的小组件(Small/Medium/Large + 点击跳转 Home) +// V2:纯色背景 + 随机文案小组件(Small/Medium/Large + 点击跳转 Home) struct EmotionProvider: TimelineProvider { + private let quotes = [ + "你已经很努力了,今天也值得被温柔对待。", + "轻轻呼吸,感受当下的每一刻。", + "所有的压力,都会在深呼吸中慢慢消散。", + "给生活一点留白,给自己一点温柔。", + "不要走得太快,等一等落下的灵魂。", + "世界虽嘈杂,但你可以拥有一颗宁静的心。", + "每一个瞬间,都是生命最好的安排。", + "抱抱自己,辛苦了,亲爱的。", + "慢一点也没关系,只要你在前行。", + "今天,你对自己微笑了吗?", + "愿你历经山河,仍觉得人间值得。", + "心简单,世界就简单;心平顺,生活就平顺。", + "即使生活偶尔晦暗,你也要成为自己的光。", + "别让琐事挤走生活的快乐,别让压力消磨奋斗的激情。" + ] + func placeholder(in context: Context) -> EmotionEntry { - EmotionEntry(date: Date()) + EmotionEntry(date: Date(), text: quotes[0]) } func getSnapshot(in context: Context, completion: @escaping (EmotionEntry) -> ()) { - completion(EmotionEntry(date: Date())) + let entry = EmotionEntry(date: Date(), text: quotes.randomElement() ?? quotes[0]) + completion(entry) } func getTimeline(in context: Context, completion: @escaping (Timeline) -> ()) { - // V1:内容写死,不做数据更新;给一个较长的刷新间隔(系统仍可能自行调度) - let entry = EmotionEntry(date: Date()) - let nextUpdate = Calendar.current.date(byAdding: .day, value: 7, to: Date()) - ?? Date().addingTimeInterval(60 * 60 * 24 * 7) - completion(Timeline(entries: [entry], policy: .after(nextUpdate))) + var entries: [EmotionEntry] = [] + let currentDate = Date() + + // 生成未来 24 小时的 6 个条目,每 4 小时更换一次随机文案 + for hourOffset in 0..<6 { + let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset * 4, to: currentDate)! + let entry = EmotionEntry(date: entryDate, text: quotes.randomElement() ?? quotes[0]) + entries.append(entry) + } + + let timeline = Timeline(entries: entries, policy: .atEnd) + completion(timeline) } } struct EmotionEntry: TimelineEntry { let date: Date + let text: String } struct EmotionWidgetView: View { @@ -30,160 +56,37 @@ struct EmotionWidgetView: View { @Environment(\.widgetFamily) var family private let title = "正念" - private let text = "你已经很努力了,今天也值得被温柔对待。" private let deepLink = URL(string: "client:///(app)/home") + + // 背景色 #F7D9BF + private let backgroundColor = Color(red: 247/255, green: 217/255, blue: 191/255) + // 文本颜色(深咖色,适合搭配浅橘色背景) + private let textColor = Color(red: 74/255, green: 52/255, blue: 40/255) var body: some View { - switch family { - case .systemSmall: - smallView() - case .systemMedium: - mediumView() - case .systemLarge: - largeView() - default: - smallView() - } - } - - // 统一的“卡片背景”风格(iOS 15 兼容) - private func cardBackground(colors: [Color]) -> some View { - ZStack { - LinearGradient( - colors: colors, - startPoint: .topLeading, - endPoint: .bottomTrailing - ) - // 轻微光斑,增加层次 - RadialGradient( - gradient: Gradient(colors: [Color.white.opacity(0.16), Color.white.opacity(0.0)]), - center: .topTrailing, - startRadius: 10, - endRadius: 180 - ) - } - .overlay( - RoundedRectangle(cornerRadius: 18, style: .continuous) - .stroke(Color.white.opacity(0.14), lineWidth: 1) - ) - .cornerRadius(18) - } - - private func chip(_ text: String) -> some View { - Text(text) - .font(.system(size: 12, weight: .semibold)) - .foregroundColor(Color.white.opacity(0.9)) - .padding(.horizontal, 10) - .padding(.vertical, 6) - .background(Color.white.opacity(0.14)) - .cornerRadius(999) - } - - private func smallView() -> some View { - ZStack { - cardBackground(colors: [ - Color(red: 0.06, green: 0.08, blue: 0.12), - Color(red: 0.13, green: 0.16, blue: 0.22), - ]) - - VStack(alignment: .leading, spacing: 10) { - HStack { - chip(title) - Spacer(minLength: 0) - } - - Text(text) - .font(.system(size: 15, weight: .semibold)) - .foregroundColor(Color.white.opacity(0.92)) - .lineSpacing(2) - .lineLimit(4) - - Spacer(minLength: 0) - - Text("点我回到 App") - .font(.system(size: 11, weight: .medium)) - .foregroundColor(Color.white.opacity(0.65)) + VStack(alignment: .center, spacing: 0) { + Spacer(minLength: 0) + + Text(entry.text) + .font(.system(size: family == .systemSmall ? 17 : 20, weight: .medium)) + .foregroundColor(textColor) + .lineSpacing(6) + .multilineTextAlignment(.center) + .minimumScaleFactor(0.7) + .fixedSize(horizontal: false, vertical: true) + + Spacer(minLength: 0) + + if family != .systemSmall { + Text("Hey Mama") + .font(.system(size: 10, weight: .semibold)) + .foregroundColor(textColor.opacity(0.3)) + .padding(.bottom, 4) } - .padding(14) - } - .widgetURL(deepLink) - } - - private func mediumView() -> some View { - ZStack { - cardBackground(colors: [ - Color(red: 0.06, green: 0.08, blue: 0.12), - Color(red: 0.09, green: 0.11, blue: 0.17), - ]) - - HStack(alignment: .top, spacing: 14) { - VStack(alignment: .leading, spacing: 10) { - chip(title) - Text(text) - .font(.system(size: 17, weight: .semibold)) - .foregroundColor(Color.white.opacity(0.92)) - .lineSpacing(3) - .lineLimit(5) - - Spacer(minLength: 0) - - Text("轻轻呼吸,回到当下") - .font(.system(size: 12, weight: .medium)) - .foregroundColor(Color.white.opacity(0.7)) - } - - // 右侧装饰区:让版面更饱满 - VStack(alignment: .trailing, spacing: 8) { - Text(entry.date, style: .time) - .font(.system(size: 12, weight: .semibold)) - .foregroundColor(Color.white.opacity(0.8)) - Spacer(minLength: 0) - Text("今日") - .font(.system(size: 28, weight: .bold)) - .foregroundColor(Color.white.opacity(0.12)) - } - } - .padding(16) - } - .widgetURL(deepLink) - } - - private func largeView() -> some View { - ZStack { - cardBackground(colors: [ - Color(red: 0.06, green: 0.08, blue: 0.12), - Color(red: 0.14, green: 0.18, blue: 0.28), - ]) - - VStack(alignment: .leading, spacing: 14) { - HStack { - chip(title) - Spacer(minLength: 0) - Text(entry.date, style: .time) - .font(.system(size: 12, weight: .semibold)) - .foregroundColor(Color.white.opacity(0.78)) - } - - Text(text) - .font(.system(size: 20, weight: .semibold)) - .foregroundColor(Color.white.opacity(0.92)) - .lineSpacing(4) - .lineLimit(8) - - Spacer(minLength: 0) - - HStack { - Text("点我回到 Home") - .font(.system(size: 12, weight: .medium)) - .foregroundColor(Color.white.opacity(0.7)) - Spacer(minLength: 0) - Text("🌿") - .font(.system(size: 18)) - .opacity(0.9) - } - } - .padding(18) } + .padding(family == .systemSmall ? 16 : 24) + .frame(maxWidth: .infinity, maxHeight: .infinity) // 强制撑开容器 + .background(backgroundColor) // 将背景色直接应用到容器上 .widgetURL(deepLink) } } @@ -194,7 +97,13 @@ struct EmotionWidget: Widget { var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: EmotionProvider()) { entry in - EmotionWidgetView(entry: entry) + if #available(iOS 17.0, *) { + EmotionWidgetView(entry: entry) + .containerBackground(Color(red: 247/255, green: 217/255, blue: 191/255), for: .widget) + } else { + EmotionWidgetView(entry: entry) + .background(Color(red: 247/255, green: 217/255, blue: 191/255)) + } } .configurationDisplayName("情绪小组件") .description("一段温柔提醒,陪你回到当下。") diff --git a/client/metro.config.js b/client/metro.config.js index ad2b03f..0e691ea 100644 --- a/client/metro.config.js +++ b/client/metro.config.js @@ -1,19 +1,20 @@ +// Metro 配置:支持 import 本地 .svg 为 React 组件 +// 说明:Expo SDK 54 + react-native-svg-transformer 的常见配置方式 const { getDefaultConfig } = require('expo/metro-config'); /** @type {import('expo/metro-config').MetroConfig} */ const config = getDefaultConfig(__dirname); -const { transformer, resolver } = config; - config.transformer = { - ...transformer, + ...config.transformer, babelTransformerPath: require.resolve('react-native-svg-transformer'), }; config.resolver = { - ...resolver, - assetExts: resolver.assetExts.filter((ext) => ext !== 'svg'), - sourceExts: [...resolver.sourceExts, 'svg', 'mjs'], + ...config.resolver, + assetExts: config.resolver.assetExts.filter((ext) => ext !== 'svg'), + sourceExts: [...config.resolver.sourceExts, 'svg'], }; module.exports = config; + diff --git a/client/package.json b/client/package.json index a457e0e..4210b49 100644 --- a/client/package.json +++ b/client/package.json @@ -14,6 +14,7 @@ "@react-navigation/native": "^7.1.8", "expo": "~54.0.32", "expo-constants": "~18.0.13", + "expo-dev-client": "^6.0.20", "expo-font": "~14.0.11", "expo-linear-gradient": "^15.0.8", "expo-linking": "~8.0.11", @@ -24,11 +25,11 @@ "expo-status-bar": "~3.0.9", "expo-web-browser": "~15.0.10", "i18next": "^25.8.0", - "pnpm": "^10.28.2", "react": "19.1.0", "react-dom": "19.1.0", "react-i18next": "^16.5.4", "react-native": "0.81.5", + "react-native-gesture-handler": "^2.30.0", "react-native-reanimated": "~4.1.1", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", @@ -38,7 +39,6 @@ "react-native-worklets": "0.5.1" }, "devDependencies": { - "@babel/plugin-transform-react-jsx": "^7.28.6", "@types/react": "~19.1.0", "react-test-renderer": "19.1.0", "typescript": "~5.9.2" diff --git a/client/pnpm-lock.yaml b/client/pnpm-lock.yaml index 8cb052d..636504a 100644 --- a/client/pnpm-lock.yaml +++ b/client/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: expo-constants: specifier: ~18.0.13 version: 18.0.13(expo@54.0.32)(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0)) + expo-dev-client: + specifier: ^6.0.20 + version: 6.0.20(expo@54.0.32) expo-font: specifier: ~14.0.11 version: 14.0.11(expo@54.0.32)(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) @@ -40,7 +43,7 @@ importers: version: 0.32.16(expo@54.0.32)(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) expo-router: specifier: ~6.0.22 - version: 6.0.22(@expo/metro-runtime@6.1.2)(@types/react@19.1.17)(expo-constants@18.0.13)(expo-linking@8.0.11)(expo@54.0.32)(react-dom@19.1.0(react@19.1.0))(react-native-reanimated@4.1.6(@babel/core@7.28.6)(react-native-worklets@0.5.1(@babel/core@7.28.6)(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 6.0.22(@expo/metro-runtime@6.1.2)(@types/react@19.1.17)(expo-constants@18.0.13)(expo-linking@8.0.11)(expo@54.0.32)(react-dom@19.1.0(react@19.1.0))(react-native-gesture-handler@2.30.0(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.6(@babel/core@7.28.6)(react-native-worklets@0.5.1(@babel/core@7.28.6)(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) expo-splash-screen: specifier: ~31.0.13 version: 31.0.13(expo@54.0.32) @@ -65,6 +68,9 @@ importers: react-native: specifier: 0.81.5 version: 0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0) + react-native-gesture-handler: + specifier: ^2.30.0 + version: 2.30.0(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) react-native-reanimated: specifier: ~4.1.1 version: 4.1.6(@babel/core@7.28.6)(react-native-worklets@0.5.1(@babel/core@7.28.6)(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) @@ -87,9 +93,6 @@ importers: specifier: 0.5.1 version: 0.5.1(@babel/core@7.28.6)(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) devDependencies: - '@babel/plugin-transform-react-jsx': - specifier: ^7.28.6 - version: 7.28.6(@babel/core@7.28.6) '@types/react': specifier: ~19.1.0 version: 19.1.17 @@ -613,6 +616,10 @@ packages: resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==} engines: {node: '>=6.9.0'} + '@egjs/hammerjs@2.0.17': + resolution: {integrity: sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==} + engines: {node: '>=0.8.0'} + '@expo/cli@54.0.22': resolution: {integrity: sha512-BTH2FCczhJLfj1cpfcKrzhKnvRLTOztgW4bVloKDqH+G3ZSohWLRFNAIz56XtdjPxBbi2/qWhGBAkl7kBon/Jw==} hasBin: true @@ -1243,6 +1250,9 @@ packages: '@types/graceful-fs@4.1.9': resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + '@types/hammerjs@2.0.46': + resolution: {integrity: sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==} + '@types/istanbul-lib-coverage@2.0.6': resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} @@ -1299,6 +1309,9 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + anser@1.4.10: resolution: {integrity: sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==} @@ -1855,6 +1868,26 @@ packages: expo: '*' react-native: '*' + expo-dev-client@6.0.20: + resolution: {integrity: sha512-5XjoVlj1OxakNxy55j/AUaGPrDOlQlB6XdHLLWAw61w5ffSpUDHDnuZzKzs9xY1eIaogOqTOQaAzZ2ddBkdXLA==} + peerDependencies: + expo: '*' + + expo-dev-launcher@6.0.20: + resolution: {integrity: sha512-a04zHEeT9sB0L5EB38fz7sNnUKJ2Ar1pXpcyl60Ki8bXPNCs9rjY7NuYrDkP/irM8+1DklMBqHpyHiLyJ/R+EA==} + peerDependencies: + expo: '*' + + expo-dev-menu-interface@2.0.0: + resolution: {integrity: sha512-BvAMPt6x+vyXpThsyjjOYyjwfjREV4OOpQkZ0tNl+nGpsPfcY9mc6DRACoWnH9KpLzyIt3BOgh3cuy/h/OxQjw==} + peerDependencies: + expo: '*' + + expo-dev-menu@7.0.18: + resolution: {integrity: sha512-4kTdlHrnZCAWCT6tZRQHSSjZ7vECFisL4T+nsG/GJDo/jcHNaOVGV5qPV9wzlTxyMk3YOPggRw4+g7Ownrg5eA==} + peerDependencies: + expo: '*' + expo-file-system@19.0.21: resolution: {integrity: sha512-s3DlrDdiscBHtab/6W1osrjGL+C2bvoInPJD7sOwmxfJ5Woynv2oc+Fz1/xVXaE/V7HE/+xrHC/H45tu6lZzzg==} peerDependencies: @@ -1868,6 +1901,9 @@ packages: react: '*' react-native: '*' + expo-json-utils@0.15.0: + resolution: {integrity: sha512-duRT6oGl80IDzH2LD2yEFWNwGIC2WkozsB6HF3cDYNoNNdUvFk6uN3YiwsTsqVM/D0z6LEAQ01/SlYvN+Fw0JQ==} + expo-keep-awake@15.0.8: resolution: {integrity: sha512-YK9M1VrnoH1vLJiQzChZgzDvVimVoriibiDIFLbQMpjYBnvyfUeHJcin/Gx1a+XgupNXy92EQJLgI/9ZuXajYQ==} peerDependencies: @@ -1893,6 +1929,11 @@ packages: expo: '*' react: '*' + expo-manifests@1.0.10: + resolution: {integrity: sha512-oxDUnURPcL4ZsOBY6X1DGWGuoZgVAFzp6PISWV7lPP2J0r8u1/ucuChBgpK7u1eLGFp6sDIPwXyEUCkI386XSQ==} + peerDependencies: + expo: '*' + expo-modules-autolinking@3.0.24: resolution: {integrity: sha512-TP+6HTwhL7orDvsz2VzauyQlXJcAWyU3ANsZ7JGL4DQu8XaZv/A41ZchbtAYLfozNA2Ya1Hzmhx65hXryBMjaQ==} hasBin: true @@ -1959,6 +2000,11 @@ packages: react: '*' react-native: '*' + expo-updates-interface@2.0.0: + resolution: {integrity: sha512-pTzAIufEZdVPKql6iMi5ylVSPqV1qbEopz9G6TSECQmnNde2nwq42PxdFBaUEd8IZJ/fdJLQnOT3m6+XJ5s7jg==} + peerDependencies: + expo: '*' + expo-web-browser@15.0.10: resolution: {integrity: sha512-fvDhW4bhmXAeWFNFiInmsGCK83PAqAcQaFyp/3pE/jbdKmFKoRCWr46uZGIfN4msLK/OODhaQ/+US7GSJNDHJg==} peerDependencies: @@ -1991,6 +2037,9 @@ packages: fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} @@ -2140,6 +2189,9 @@ packages: hermes-parser@0.32.0: resolution: {integrity: sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==} + hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + hosted-git-info@7.0.2: resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} engines: {node: ^16.14.0 || >=18.0.0} @@ -2329,6 +2381,9 @@ packages: json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -2885,12 +2940,21 @@ packages: typescript: optional: true + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} react-is@19.2.4: resolution: {integrity: sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==} + react-native-gesture-handler@2.30.0: + resolution: {integrity: sha512-5YsnKHGa0X9C8lb5oCnKm0fLUPM6CRduvUUw2Bav4RIj/C3HcFh4RIUnF8wgG6JQWCL1//gRx4v+LVWgcIQdGA==} + peerDependencies: + react: '*' + react-native: '*' + react-native-is-edge-to-edge@1.2.1: resolution: {integrity: sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q==} peerDependencies: @@ -4189,6 +4253,10 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@egjs/hammerjs@2.0.17': + dependencies: + '@types/hammerjs': 2.0.46 + '@expo/cli@54.0.22(expo-router@6.0.22)(expo@54.0.32)(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))': dependencies: '@0no-co/graphql.web': 1.2.0 @@ -4256,7 +4324,7 @@ snapshots: wrap-ansi: 7.0.0 ws: 8.19.0 optionalDependencies: - expo-router: 6.0.22(@expo/metro-runtime@6.1.2)(@types/react@19.1.17)(expo-constants@18.0.13)(expo-linking@8.0.11)(expo@54.0.32)(react-dom@19.1.0(react@19.1.0))(react-native-reanimated@4.1.6(@babel/core@7.28.6)(react-native-worklets@0.5.1(@babel/core@7.28.6)(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo-router: 6.0.22(@expo/metro-runtime@6.1.2)(@types/react@19.1.17)(expo-constants@18.0.13)(expo-linking@8.0.11)(expo@54.0.32)(react-dom@19.1.0(react@19.1.0))(react-native-gesture-handler@2.30.0(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.6(@babel/core@7.28.6)(react-native-worklets@0.5.1(@babel/core@7.28.6)(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) react-native: 0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0) transitivePeerDependencies: - bufferutil @@ -5086,6 +5154,8 @@ snapshots: dependencies: '@types/node': 25.1.0 + '@types/hammerjs@2.0.46': {} + '@types/istanbul-lib-coverage@2.0.6': {} '@types/istanbul-lib-report@3.0.3': @@ -5141,6 +5211,13 @@ snapshots: agent-base@7.1.4: {} + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + anser@1.4.10: {} ansi-escapes@4.3.2: @@ -5734,6 +5811,35 @@ snapshots: transitivePeerDependencies: - supports-color + expo-dev-client@6.0.20(expo@54.0.32): + dependencies: + expo: 54.0.32(@babel/core@7.28.6)(@expo/metro-runtime@6.1.2)(expo-router@6.0.22)(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo-dev-launcher: 6.0.20(expo@54.0.32) + expo-dev-menu: 7.0.18(expo@54.0.32) + expo-dev-menu-interface: 2.0.0(expo@54.0.32) + expo-manifests: 1.0.10(expo@54.0.32) + expo-updates-interface: 2.0.0(expo@54.0.32) + transitivePeerDependencies: + - supports-color + + expo-dev-launcher@6.0.20(expo@54.0.32): + dependencies: + ajv: 8.17.1 + expo: 54.0.32(@babel/core@7.28.6)(@expo/metro-runtime@6.1.2)(expo-router@6.0.22)(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo-dev-menu: 7.0.18(expo@54.0.32) + expo-manifests: 1.0.10(expo@54.0.32) + transitivePeerDependencies: + - supports-color + + expo-dev-menu-interface@2.0.0(expo@54.0.32): + dependencies: + expo: 54.0.32(@babel/core@7.28.6)(@expo/metro-runtime@6.1.2)(expo-router@6.0.22)(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + + expo-dev-menu@7.0.18(expo@54.0.32): + dependencies: + expo: 54.0.32(@babel/core@7.28.6)(@expo/metro-runtime@6.1.2)(expo-router@6.0.22)(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo-dev-menu-interface: 2.0.0(expo@54.0.32) + expo-file-system@19.0.21(expo@54.0.32)(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0)): dependencies: expo: 54.0.32(@babel/core@7.28.6)(@expo/metro-runtime@6.1.2)(expo-router@6.0.22)(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) @@ -5746,6 +5852,8 @@ snapshots: react: 19.1.0 react-native: 0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0) + expo-json-utils@0.15.0: {} + expo-keep-awake@15.0.8(expo@54.0.32)(react@19.1.0): dependencies: expo: 54.0.32(@babel/core@7.28.6)(@expo/metro-runtime@6.1.2)(expo-router@6.0.22)(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) @@ -5773,6 +5881,14 @@ snapshots: react: 19.1.0 rtl-detect: 1.1.2 + expo-manifests@1.0.10(expo@54.0.32): + dependencies: + '@expo/config': 12.0.13 + expo: 54.0.32(@babel/core@7.28.6)(@expo/metro-runtime@6.1.2)(expo-router@6.0.22)(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo-json-utils: 0.15.0 + transitivePeerDependencies: + - supports-color + expo-modules-autolinking@3.0.24: dependencies: '@expo/spawn-async': 1.7.2 @@ -5802,7 +5918,7 @@ snapshots: transitivePeerDependencies: - supports-color - expo-router@6.0.22(@expo/metro-runtime@6.1.2)(@types/react@19.1.17)(expo-constants@18.0.13)(expo-linking@8.0.11)(expo@54.0.32)(react-dom@19.1.0(react@19.1.0))(react-native-reanimated@4.1.6(@babel/core@7.28.6)(react-native-worklets@0.5.1(@babel/core@7.28.6)(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + expo-router@6.0.22(@expo/metro-runtime@6.1.2)(@types/react@19.1.17)(expo-constants@18.0.13)(expo-linking@8.0.11)(expo@54.0.32)(react-dom@19.1.0(react@19.1.0))(react-native-gesture-handler@2.30.0(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.6(@babel/core@7.28.6)(react-native-worklets@0.5.1(@babel/core@7.28.6)(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): dependencies: '@expo/metro-runtime': 6.1.2(expo@54.0.32)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) '@expo/schema-utils': 0.1.8 @@ -5836,6 +5952,7 @@ snapshots: vaul: 1.1.2(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) optionalDependencies: react-dom: 19.1.0(react@19.1.0) + react-native-gesture-handler: 2.30.0(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) react-native-reanimated: 4.1.6(@babel/core@7.28.6)(react-native-worklets@0.5.1(@babel/core@7.28.6)(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) react-native-web: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) transitivePeerDependencies: @@ -5859,6 +5976,10 @@ snapshots: react-native: 0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0) react-native-is-edge-to-edge: 1.2.1(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo-updates-interface@2.0.0(expo@54.0.32): + dependencies: + expo: 54.0.32(@babel/core@7.28.6)(@expo/metro-runtime@6.1.2)(expo-router@6.0.22)(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo-web-browser@15.0.10(expo@54.0.32)(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0)): dependencies: expo: 54.0.32(@babel/core@7.28.6)(@expo/metro-runtime@6.1.2)(expo-router@6.0.22)(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) @@ -5905,6 +6026,8 @@ snapshots: fast-json-stable-stringify@2.1.0: {} + fast-uri@3.1.0: {} + fb-watchman@2.0.2: dependencies: bser: 2.1.1 @@ -6052,6 +6175,10 @@ snapshots: dependencies: hermes-estree: 0.32.0 + hoist-non-react-statics@3.3.2: + dependencies: + react-is: 16.13.1 + hosted-git-info@7.0.2: dependencies: lru-cache: 10.4.3 @@ -6271,6 +6398,8 @@ snapshots: json-parse-even-better-errors@2.3.1: {} + json-schema-traverse@1.0.0: {} + json5@2.2.3: {} kleur@3.0.3: {} @@ -6867,10 +6996,20 @@ snapshots: react-native: 0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0) typescript: 5.9.3 + react-is@16.13.1: {} + react-is@18.3.1: {} react-is@19.2.4: {} + react-native-gesture-handler@2.30.0(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + dependencies: + '@egjs/hammerjs': 2.0.17 + hoist-non-react-statics: 3.3.2 + invariant: 2.2.4 + react: 19.1.0 + react-native: 0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0) + react-native-is-edge-to-edge@1.2.1(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): dependencies: react: 19.1.0 diff --git a/client/src/i18n/locales/zh-CN.json b/client/src/i18n/locales/zh-CN.json index 2e9aab6..41a300e 100644 --- a/client/src/i18n/locales/zh-CN.json +++ b/client/src/i18n/locales/zh-CN.json @@ -3,6 +3,7 @@ "ok": "确定", "cancel": "取消", "error": "错误", + "notice": "提示", "openLinkError": "无法打开链接", "back": "返回" }, diff --git a/client/src/storage/appStorage.ts b/client/src/storage/appStorage.ts index 3d0f399..455038a 100644 --- a/client/src/storage/appStorage.ts +++ b/client/src/storage/appStorage.ts @@ -69,14 +69,29 @@ export async function setReaction(contentId: string, reaction: Reaction): Promis await setJson(KEY_CONTENT_REACTIONS, next); } -export async function getFavorites(): Promise { - return await getJson(KEY_FAVORITES_ITEMS, []); +export type FavoriteItem = { + id: string; + date: string; + themeMode: ThemeMode; + background: string; // 颜色值或图片路径 +}; + +export async function getFavorites(): Promise { + return await getJson(KEY_FAVORITES_ITEMS, []); } -export async function addFavorite(contentId: string): Promise { +export async function addFavorite(item: FavoriteItem): Promise { const list = await getFavorites(); - if (list.includes(contentId)) return; - await setJson(KEY_FAVORITES_ITEMS, [...list, contentId]); + if (list.some(i => i.id === item.id)) return; + const newList = [item, ...list]; + console.log('Adding to favorites, new list size:', newList.length); + await setJson(KEY_FAVORITES_ITEMS, newList); +} + +export async function removeFavorite(contentId: string): Promise { + const list = await getFavorites(); + const next = list.filter(item => item.id !== contentId); + await setJson(KEY_FAVORITES_ITEMS, next); } export async function getConsentAccepted(): Promise {