feature:基本功能
@@ -1,10 +1,7 @@
|
||||
import { Stack } from 'expo-router';
|
||||
import { Pressable, Text } from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useRouter } from 'expo-router';
|
||||
|
||||
export default function AppLayout() {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
@@ -16,26 +13,9 @@ export default function AppLayout() {
|
||||
<Stack.Screen
|
||||
name="home"
|
||||
options={{
|
||||
title: t('home.title'),
|
||||
headerRight: () => (
|
||||
<Pressable
|
||||
onPress={() => router.push('/(app)/settings')}
|
||||
hitSlop={10}
|
||||
>
|
||||
<Text>{t('home.settings')}</Text>
|
||||
</Pressable>
|
||||
),
|
||||
headerLeft: () => (
|
||||
<Pressable onPress={() => router.push('/(app)/favorites')} hitSlop={10}>
|
||||
<Text>{t('home.favorites')}</Text>
|
||||
</Pressable>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="favorites"
|
||||
options={{
|
||||
title: t('favorites.title'),
|
||||
// 卡片页标题按设计留空(右上角为 icon 按钮)
|
||||
title: '',
|
||||
headerShadowVisible: false,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { FlatList, StyleSheet, Text, View } from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { MOCK_CONTENT } from '@/src/constants/mockContent';
|
||||
import { getFavorites } from '@/src/storage/appStorage';
|
||||
|
||||
export default function FavoritesScreen() {
|
||||
const { t } = useTranslation();
|
||||
const [ids, setIds] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
const list = await getFavorites();
|
||||
if (!cancelled) setIds(list);
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
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]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{items.length === 0 ? (
|
||||
<Text style={styles.empty}>{t('favorites.empty')}</Text>
|
||||
) : (
|
||||
<FlatList
|
||||
data={items}
|
||||
keyExtractor={(it) => it.id}
|
||||
contentContainerStyle={styles.list}
|
||||
renderItem={({ item }) => (
|
||||
<View style={styles.row}>
|
||||
<Text style={styles.text}>{item.text}</Text>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, padding: 16 },
|
||||
empty: { color: '#6B7280', fontSize: 16, textAlign: 'center', marginTop: 40 },
|
||||
list: { gap: 12, paddingBottom: 24 },
|
||||
row: {
|
||||
borderRadius: 14,
|
||||
padding: 16,
|
||||
backgroundColor: '#F9FAFB',
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: '#E5E7EB',
|
||||
},
|
||||
text: { color: '#111827', fontSize: 16, lineHeight: 22 },
|
||||
});
|
||||
|
||||
@@ -1,45 +1,220 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
import { useEffect, useLayoutEffect, useMemo, useState } from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import { Pressable } from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigation } from 'expo-router';
|
||||
import Animated, {
|
||||
Easing,
|
||||
runOnJS,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withSequence,
|
||||
withTiming,
|
||||
} from 'react-native-reanimated';
|
||||
|
||||
import { MOCK_CONTENT } from '@/src/constants/mockContent';
|
||||
import { addFavorite, setReaction } from '@/src/storage/appStorage';
|
||||
import {
|
||||
addFavorite,
|
||||
getThemeMode,
|
||||
getUserProfile,
|
||||
setReaction,
|
||||
setThemeMode,
|
||||
type ThemeMode,
|
||||
} from '@/src/storage/appStorage';
|
||||
|
||||
import ProfileModal from '@/components/home/ProfileModal';
|
||||
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';
|
||||
|
||||
export default function HomeScreen() {
|
||||
const { t } = useTranslation();
|
||||
const navigation = useNavigation();
|
||||
const [index, setIndex] = useState(0);
|
||||
const [themeMode, setThemeModeState] = useState<ThemeMode>('scenery');
|
||||
const [themeOpen, setThemeOpen] = useState(false);
|
||||
const [profileOpen, setProfileOpen] = useState(false);
|
||||
const [profileName, setProfileName] = useState<string | undefined>(undefined);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [likeFilled, setLikeFilled] = useState(false);
|
||||
|
||||
const item = useMemo(() => MOCK_CONTENT[index % MOCK_CONTENT.length], [index]);
|
||||
|
||||
async function onLike() {
|
||||
await setReaction(item.id, 'like');
|
||||
await addFavorite(item.id);
|
||||
setIndex((i) => i + 1);
|
||||
useEffect(() => {
|
||||
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';
|
||||
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerShadowVisible: false,
|
||||
headerStyle: { backgroundColor },
|
||||
headerRight: () => (
|
||||
<View style={styles.headerRight}>
|
||||
<CircleIconButton
|
||||
onPress={() => setThemeOpen(true)}
|
||||
accessibilityLabel={t('home.theme')}
|
||||
>
|
||||
<ThemeIcon width={18} height={18} />
|
||||
</CircleIconButton>
|
||||
<CircleIconButton
|
||||
onPress={() => setProfileOpen(true)}
|
||||
accessibilityLabel={t('home.profile')}
|
||||
>
|
||||
<MyIcon width={18} height={18} />
|
||||
</CircleIconButton>
|
||||
</View>
|
||||
),
|
||||
});
|
||||
}, [backgroundColor, navigation, t]);
|
||||
|
||||
const likeScale = useSharedValue(1);
|
||||
const hateScale = useSharedValue(1);
|
||||
|
||||
const likeAnimatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ scale: likeScale.value }],
|
||||
}));
|
||||
|
||||
const hateAnimatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ scale: hateScale.value }],
|
||||
}));
|
||||
|
||||
function playLikeAnimationAndThen(next: () => void) {
|
||||
setLikeFilled(true);
|
||||
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)();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async function onDislike() {
|
||||
await setReaction(item.id, 'dislike');
|
||||
setIndex((i) => i + 1);
|
||||
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);
|
||||
setThemeOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={[styles.container, { backgroundColor }]}>
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.text}>{item.text}</Text>
|
||||
<Animated.Text style={styles.text}>{item.text}</Animated.Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.actions}>
|
||||
<Pressable style={[styles.button, styles.dislike]} onPress={onDislike}>
|
||||
<Text style={styles.buttonText}>{t('home.dislike')}</Text>
|
||||
</Pressable>
|
||||
<Pressable style={[styles.button, styles.like]} onPress={onLike}>
|
||||
<Text style={styles.buttonText}>{t('home.like')}</Text>
|
||||
</Pressable>
|
||||
<Animated.View style={[styles.reactionButton, hateAnimatedStyle]}>
|
||||
<Pressable
|
||||
onPress={onPressHate}
|
||||
onPressIn={() => (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}
|
||||
>
|
||||
<HateIcon width={30} height={30} />
|
||||
</Pressable>
|
||||
</Animated.View>
|
||||
|
||||
<Animated.View style={[styles.reactionButton, likeAnimatedStyle]}>
|
||||
<Pressable
|
||||
onPress={onPressLike}
|
||||
onPressIn={() => (likeScale.value = withTiming(0.92, { duration: 80 }))}
|
||||
onPressOut={() => (likeScale.value = withTiming(1, { duration: 120 }))}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={t('home.like')}
|
||||
hitSlop={10}
|
||||
style={styles.reactionInner}
|
||||
>
|
||||
{likeFilled ? (
|
||||
<LikeFilledIcon width={30} height={30} />
|
||||
) : (
|
||||
<LikeOutlineIcon width={30} height={30} />
|
||||
)}
|
||||
</Pressable>
|
||||
</Animated.View>
|
||||
</View>
|
||||
|
||||
<ThemeModal visible={themeOpen} mode={themeMode} onSelect={onSelectTheme} onClose={() => setThemeOpen(false)} />
|
||||
<ProfileModal
|
||||
visible={profileOpen}
|
||||
name={profileName}
|
||||
onClose={() => setProfileOpen(false)}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function CircleIconButton({
|
||||
onPress,
|
||||
accessibilityLabel,
|
||||
children,
|
||||
}: {
|
||||
onPress: () => void;
|
||||
accessibilityLabel: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
hitSlop={10}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
style={styles.circleBtn}
|
||||
>
|
||||
{children}
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
@@ -47,30 +222,51 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
gap: 16,
|
||||
},
|
||||
headerRight: {
|
||||
flexDirection: 'row',
|
||||
gap: 10,
|
||||
paddingRight: 10,
|
||||
},
|
||||
circleBtn: {
|
||||
width: 34,
|
||||
height: 34,
|
||||
borderRadius: 17,
|
||||
backgroundColor: 'rgba(255,255,255,0.75)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
card: {
|
||||
borderRadius: 16,
|
||||
padding: 20,
|
||||
backgroundColor: '#F6F7FB',
|
||||
backgroundColor: 'rgba(255,255,255,0.65)',
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: '#E5E7EB',
|
||||
},
|
||||
text: {
|
||||
fontSize: 20,
|
||||
lineHeight: 28,
|
||||
color: '#111827',
|
||||
color: '#5E2A28',
|
||||
fontWeight: '700',
|
||||
},
|
||||
actions: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
button: {
|
||||
flex: 1,
|
||||
paddingVertical: 14,
|
||||
borderRadius: 14,
|
||||
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',
|
||||
},
|
||||
like: { backgroundColor: '#16A34A' },
|
||||
dislike: { backgroundColor: '#EF4444' },
|
||||
buttonText: { color: 'white', fontSize: 16, fontWeight: '600' },
|
||||
});
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ export default function OnboardingLayout() {
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerTitleAlign: 'center',
|
||||
headerShown: false, // 隐藏原生导航栏,使用自定义布局
|
||||
}}
|
||||
>
|
||||
<Stack.Screen name="onboarding" options={{ title: t('onboarding.title') }} />
|
||||
|
||||
@@ -1,104 +1,68 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { setOnboardingCompleted } from '@/src/storage/appStorage';
|
||||
|
||||
type OnboardingPage = {
|
||||
title: string;
|
||||
desc: string;
|
||||
};
|
||||
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';
|
||||
|
||||
export default function OnboardingScreen() {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// 3–5 页:这里默认 4 页,后续可按产品调整为 3 或 5
|
||||
const pages = useMemo<OnboardingPage[]>(
|
||||
() => [
|
||||
{ title: t('onboarding.q1Title'), desc: t('onboarding.q1Desc') },
|
||||
{ title: t('onboarding.q2Title'), desc: t('onboarding.q2Desc') },
|
||||
{ title: t('onboarding.q3Title'), desc: t('onboarding.q3Desc') },
|
||||
{ title: t('onboarding.q4Title'), desc: t('onboarding.q4Desc') }
|
||||
],
|
||||
[t]
|
||||
);
|
||||
|
||||
const total = pages.length;
|
||||
const [step, setStep] = useState(0);
|
||||
const page = pages[Math.min(step, total - 1)];
|
||||
const [name, setName] = useState('');
|
||||
const [intents, setIntents] = useState<string[]>([]);
|
||||
|
||||
async function finishAndNext() {
|
||||
async function onFinish() {
|
||||
// Save whatever input we have
|
||||
await setUserProfile({ name, intents });
|
||||
await setOnboardingCompleted(true);
|
||||
router.replace('/(onboarding)/push-prompt');
|
||||
}
|
||||
|
||||
async function onNext() {
|
||||
if (step >= total - 1) {
|
||||
await finishAndNext();
|
||||
return;
|
||||
if (step === 0) {
|
||||
setStep(1);
|
||||
} else {
|
||||
await onFinish();
|
||||
}
|
||||
setStep((s) => s + 1);
|
||||
}
|
||||
|
||||
async function onSkipPage() {
|
||||
// 每页可跳过:直接进入下一页(最后一页则结束)
|
||||
await onNext();
|
||||
async function onSkip() {
|
||||
// Skip logic: move to next step regardless of input
|
||||
if (step === 0) {
|
||||
setStep(1);
|
||||
} else {
|
||||
await onFinish();
|
||||
}
|
||||
}
|
||||
|
||||
async function onSkipAll() {
|
||||
// 一键跳过整个 Onboarding
|
||||
await finishAndNext();
|
||||
}
|
||||
// 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;
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.progress}>
|
||||
{t('onboarding.progress', { current: step + 1, total })}
|
||||
</Text>
|
||||
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.title}>{page.title}</Text>
|
||||
<Text style={styles.desc}>{page.desc}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.actions}>
|
||||
<Pressable style={[styles.btn, styles.secondary]} onPress={onSkipPage}>
|
||||
<Text style={[styles.btnText, styles.secondaryText]}>{t('onboarding.skip')}</Text>
|
||||
</Pressable>
|
||||
<Pressable style={[styles.btn, styles.primary]} onPress={onNext}>
|
||||
<Text style={styles.btnText}>{t('onboarding.next')}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<Pressable style={styles.skipAll} onPress={onSkipAll}>
|
||||
<Text style={styles.skipAllText}>{t('onboarding.skipAll')}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
<OnboardingLayout
|
||||
onSkip={onSkip}
|
||||
onNext={onNext}
|
||||
nextEnabled={nextEnabled}
|
||||
>
|
||||
{step === 0 ? (
|
||||
<NameInputStep
|
||||
value={name}
|
||||
onChangeText={setName}
|
||||
onSubmitEditing={name.trim().length > 0 ? onNext : undefined}
|
||||
/>
|
||||
) : (
|
||||
<IntentSelectionStep
|
||||
selectedIds={intents}
|
||||
onToggle={(id) => {
|
||||
setIntents(prev =>
|
||||
prev.includes(id)
|
||||
? prev.filter(i => i !== id)
|
||||
: [...prev, id]
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</OnboardingLayout>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, padding: 20, justifyContent: 'center', gap: 16 },
|
||||
progress: { textAlign: 'center', color: '#6B7280' },
|
||||
card: {
|
||||
borderRadius: 18,
|
||||
padding: 20,
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: '#E5E7EB',
|
||||
gap: 10
|
||||
},
|
||||
title: { fontSize: 22, fontWeight: '700', color: '#111827' },
|
||||
desc: { fontSize: 16, lineHeight: 22, color: '#374151' },
|
||||
actions: { flexDirection: 'row', gap: 12 },
|
||||
btn: { flex: 1, paddingVertical: 14, borderRadius: 14, alignItems: 'center' },
|
||||
primary: { backgroundColor: '#111827' },
|
||||
secondary: { backgroundColor: '#F3F4F6' },
|
||||
btnText: { fontSize: 16, fontWeight: '600', color: '#FFFFFF' },
|
||||
secondaryText: { color: '#111827' },
|
||||
skipAll: { alignItems: 'center', paddingTop: 10 },
|
||||
skipAllText: { color: '#6B7280', textDecorationLine: 'underline' }
|
||||
});
|
||||
|
||||
|
||||
10
client/app/(splash)/_layout.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Stack } from 'expo-router';
|
||||
import React from 'react';
|
||||
|
||||
export default function SplashLayout() {
|
||||
return (
|
||||
<Stack screenOptions={{ headerShown: false }}>
|
||||
<Stack.Screen name="splash" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
163
client/app/(splash)/splash.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
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 { 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';
|
||||
|
||||
const { width, height } = Dimensions.get('window');
|
||||
|
||||
export default function SplashScreen() {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const [showConsent, setShowConsent] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
checkConsent();
|
||||
}, []);
|
||||
|
||||
const checkConsent = async () => {
|
||||
const accepted = await getConsentAccepted();
|
||||
setShowConsent(!accepted);
|
||||
if (accepted) {
|
||||
// 如果已经同意过,直接跳转到首页分发
|
||||
// 注意:这里需要与 app/index.tsx 配合,如果 app/index.tsx 已经判断了 consent=true 不会跳过来,
|
||||
// 那么这里其实是防守。
|
||||
// 但如果用户是通过 deep link 或其他方式强制进入 splash,这个逻辑会把他们送走。
|
||||
router.replace('/');
|
||||
}
|
||||
};
|
||||
|
||||
const handleAgree = async () => {
|
||||
await setConsentAccepted(true);
|
||||
setShowConsent(false);
|
||||
router.replace('/');
|
||||
};
|
||||
|
||||
const openLink = async (url: string) => {
|
||||
try {
|
||||
await WebBrowser.openBrowserAsync(url);
|
||||
} catch (error) {
|
||||
Alert.alert(t('common.error'), t('common.openLinkError'));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Image
|
||||
source={require('../../assets/images/index/index_flowers.png')}
|
||||
style={styles.image}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
|
||||
<View style={styles.contentContainer}>
|
||||
<Text style={styles.title}>{t('consent.title')}</Text>
|
||||
<Text style={styles.subtitle}>{t('consent.subtitle')}</Text>
|
||||
</View>
|
||||
|
||||
<SafeAreaView style={styles.bottomContainer} edges={['bottom']}>
|
||||
{showConsent && (
|
||||
<>
|
||||
<TouchableOpacity onPress={handleAgree} activeOpacity={0.8}>
|
||||
<LinearGradient
|
||||
colors={['#F69F7B', '#F99CC0']}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0 }}
|
||||
style={styles.button}
|
||||
>
|
||||
<Text style={styles.buttonText}>{t('consent.agree')}</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={styles.linksContainer}>
|
||||
<TouchableOpacity onPress={() => openLink('https://example.com/privacy')}>
|
||||
<Text style={styles.linkText}>{t('consent.privacy')}</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.divider} />
|
||||
<TouchableOpacity onPress={() => openLink('https://example.com/terms')}>
|
||||
<Text style={styles.linkText}>{t('consent.terms')}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#FFF9F0', // 假设的背景色,根据图片调整
|
||||
alignItems: 'center',
|
||||
},
|
||||
image: {
|
||||
width: width * 0.8,
|
||||
height: height * 0.5,
|
||||
marginTop: height * 0.1,
|
||||
},
|
||||
contentContainer: {
|
||||
alignItems: 'center',
|
||||
marginTop: 20,
|
||||
paddingHorizontal: 30,
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
color: '#4A3427', // 深褐色
|
||||
marginBottom: 10,
|
||||
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',
|
||||
},
|
||||
bottomContainer: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
paddingBottom: 20,
|
||||
},
|
||||
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',
|
||||
},
|
||||
linksContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 10,
|
||||
},
|
||||
linkText: {
|
||||
fontSize: 12,
|
||||
color: '#999',
|
||||
textDecorationLine: 'underline',
|
||||
},
|
||||
divider: {
|
||||
width: 1,
|
||||
height: 12,
|
||||
backgroundColor: '#CCC',
|
||||
marginHorizontal: 15,
|
||||
},
|
||||
});
|
||||
@@ -2,10 +2,10 @@ import { useEffect } from 'react';
|
||||
import { ActivityIndicator, StyleSheet, View } from 'react-native';
|
||||
import { useRouter } from 'expo-router';
|
||||
|
||||
import { getOnboardingCompleted } from '@/src/storage/appStorage';
|
||||
import { getOnboardingCompleted, getConsentAccepted } from '@/src/storage/appStorage';
|
||||
|
||||
/**
|
||||
* 启动分发:根据 onboarding 状态跳转
|
||||
* 启动分发:根据 consent 和 onboarding 状态跳转
|
||||
*/
|
||||
export default function Index() {
|
||||
const router = useRouter();
|
||||
@@ -13,8 +13,17 @@ export default function Index() {
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
const completed = await getOnboardingCompleted();
|
||||
// 1. 检查是否同意协议
|
||||
const consentAccepted = await getConsentAccepted();
|
||||
if (cancelled) return;
|
||||
|
||||
if (!consentAccepted) {
|
||||
router.replace('/(splash)/splash');
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 检查 Onboarding
|
||||
const completed = await getOnboardingCompleted();
|
||||
router.replace(completed ? '/(app)/home' : '/(onboarding)/onboarding');
|
||||
})();
|
||||
return () => {
|
||||
|
||||
BIN
client/assets/images/home/Profile/Default_avatar.png
Normal file
|
After Width: | Height: | Size: 93 KiB |
3
client/assets/images/home/Profile/language.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.49467 13.5033C8.78067 16.7229 9.77067 19.726 11.428 21.4441L11.9707 22L12.5133 21.4441C14.2073 19.7044 15.19 16.7013 15.4467 13.5033C14.3027 13.5827 13.122 13.626 11.9413 13.626C10.7607 13.626 9.63867 13.5899 8.49467 13.5033ZM15.5933 10.9694C15.5053 7.22276 14.486 3.5483 12.5427 1.58474L12 1L11.4573 1.55586C9.514 3.55552 8.524 7.22276 8.40667 10.9405C10.7955 11.1313 13.1957 11.141 15.586 10.9694H15.5933ZM22.4427 9.52561L22.7873 9.35957C22.4041 7.50584 21.5342 5.78328 20.2643 4.36346C18.9944 2.94363 17.3687 1.87607 15.5493 1.2671C17.1553 3.72155 18.0133 7.16501 18.1307 10.6518C19.614 10.4828 21.0657 10.1086 22.4427 9.54005V9.52561ZM18.0867 13.2362C17.9566 16.2416 17.0777 19.1695 15.5273 21.7618C17.6163 21.0689 19.4457 19.7741 20.7781 18.0454C22.1106 16.3167 22.8847 14.2337 23 12.0667C21.415 12.6465 19.7657 13.0391 18.0867 13.2362V13.2362ZM1 12.0667C1.11534 14.2337 1.88942 16.3167 3.22187 18.0454C4.55432 19.7741 6.38366 21.0689 8.47267 21.7618C6.92232 19.1695 6.0434 16.2416 5.91333 13.2362C4.23432 13.0391 2.58501 12.6465 1 12.0667V12.0667ZM5.84 10.6879C5.97933 7.17944 6.86667 3.72877 8.45067 1.2671C6.6331 1.87484 5.00872 2.94021 3.73896 4.35734C2.46919 5.77447 1.59825 7.49403 1.21267 9.34514L1.55733 9.51117C2.93096 10.102 4.38274 10.4982 5.86933 10.6879H5.84Z" fill="#EEB054"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
3
client/assets/images/home/Profile/mylike.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="37" height="33" viewBox="0 0 37 33" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M27.38 0C25.4752 0.00420958 23.6144 0.597538 22.0335 1.70479C21.0003 2.42849 20.117 3.35044 19.4272 4.41503C19.0322 5.02468 17.9678 5.02469 17.5728 4.41504C16.8819 3.34867 15.9968 2.42544 14.9614 1.70122C13.3781 0.593805 11.5146 0.00166536 9.60767 0C4.30433 0 0 5.51285 0 11.0514C0 23.1565 16.0333 33 18.5 33C20.9667 33 37 23.1565 37 11.0514C37 5.51285 32.6957 0 27.38 0Z" fill="#FE3C3C" fill-opacity="0.76"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 522 B |
3
client/assets/images/home/Profile/privacy Policy.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.48 5.6925L21.27 4.035L12 0.75L2.73003 4.035L2.52003 5.685C2.44503 6.24 0.915026 19.2525 11.0775 22.935L12 23.25L12.915 22.92C23.0475 19.275 21.555 6.2475 21.48 5.6925ZM17.85 9.1425L12.8925 17.0925L12.825 17.1975C12.5406 17.6509 12.0883 17.9733 11.5669 18.0941C11.0455 18.215 10.4974 18.1245 10.0425 17.8425C9.83101 17.7097 9.64547 17.5394 9.49503 17.34L6.29253 13.0725C6.01107 12.791 5.85294 12.4093 5.85294 12.0112C5.85294 11.6132 6.01107 11.2315 6.29253 10.95C6.57399 10.6685 6.95573 10.5104 7.35378 10.5104C7.75182 10.5104 8.13357 10.6685 8.41503 10.95L10.7625 12.75L15.45 7.3875C15.6936 7.0877 16.0435 6.89345 16.4268 6.84521C16.8101 6.79696 17.1972 6.89845 17.5075 7.12853C17.8178 7.3586 18.0274 7.69953 18.0926 8.08029C18.1578 8.46105 18.0736 8.85228 17.8575 9.1725L17.85 9.1425Z" fill="#879CE7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 920 B |
3
client/assets/images/home/Profile/remind.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 22.7325C12.5691 22.7303 13.1259 22.5663 13.6053 22.2595C14.0847 21.9527 14.467 21.5159 14.7075 21H9.29246C9.53293 21.5159 9.91518 21.9527 10.3946 22.2595C10.874 22.5663 11.4308 22.7303 12 22.7325ZM21.75 16.7325C20.1075 14.835 19.8 12.9825 19.8 10.7325C19.7892 8.98974 19.1911 7.30149 18.1024 5.94058C17.0137 4.57967 15.4979 3.62559 13.8 3.23252C13.8039 3.16258 13.8039 3.09247 13.8 3.02252C13.8 2.52524 13.6024 2.04833 13.2508 1.6967C12.8992 1.34507 12.4222 1.14752 11.925 1.14752C11.4277 1.14752 10.9508 1.34507 10.5991 1.6967C10.2475 2.04833 10.05 2.52524 10.05 3.02252C10.046 3.09247 10.046 3.16258 10.05 3.23252C8.35205 3.62559 6.83626 4.57967 5.74753 5.94058C4.6588 7.30149 4.06073 8.98974 4.04996 10.7325C4.2115 12.9108 3.51125 15.0654 2.09996 16.7325C1.41746 17.6625 1.83746 19.5 3.31496 19.5H20.685C22.1625 19.5 22.59 17.6625 21.75 16.725V16.7325ZM15.75 13.5H13.5V15.75C13.5 16.1478 13.3419 16.5294 13.0606 16.8107C12.7793 17.092 12.3978 17.25 12 17.25C11.6021 17.25 11.2206 17.092 10.9393 16.8107C10.658 16.5294 10.5 16.1478 10.5 15.75V13.5H8.24996C7.85214 13.5 7.4706 13.342 7.1893 13.0607C6.908 12.7794 6.74996 12.3978 6.74996 12C6.74996 11.6022 6.908 11.2207 7.1893 10.9394C7.4706 10.6581 7.85214 10.5 8.24996 10.5H10.5V8.25002C10.5 7.8522 10.658 7.47067 10.9393 7.18936C11.2206 6.90806 11.6021 6.75002 12 6.75002C12.3978 6.75002 12.7793 6.90806 13.0606 7.18936C13.3419 7.47067 13.5 7.8522 13.5 8.25002V10.5H15.75C16.1478 10.5 16.5293 10.6581 16.8106 10.9394C17.0919 11.2207 17.25 11.6022 17.25 12C17.25 12.3978 17.0919 12.7794 16.8106 13.0607C16.5293 13.342 16.1478 13.5 15.75 13.5Z" fill="#F59250"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
6
client/assets/images/home/Profile/small_component.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="39" height="39" viewBox="0 0 39 39" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.28564 2.592C3.28564 1.16048 4.44612 0 5.87764 0H16.5581C17.9896 0 19.1501 1.16048 19.1501 2.592V13.2725C19.1501 14.704 17.9896 15.8645 16.5581 15.8645H5.87764C4.44612 15.8645 3.28564 14.704 3.28564 13.2725V2.592Z" fill="#A360F5"/>
|
||||
<path d="M22.811 2.592C22.811 1.16048 23.9715 0 25.403 0H36.0835C37.515 0 38.6755 1.16048 38.6755 2.592V13.2725C38.6755 14.704 37.515 15.8645 36.0835 15.8645H25.403C23.9715 15.8645 22.811 14.704 22.811 13.2725V2.592Z" fill="#FB84D3"/>
|
||||
<path d="M1.83282 29.2908C0.820581 28.2786 0.820582 26.6374 1.83282 25.6252L9.38505 18.073C10.3973 17.0607 12.0385 17.0607 13.0507 18.073L20.6029 25.6252C21.6152 26.6374 21.6152 28.2786 20.6029 29.2908L13.0507 36.8431C12.0384 37.8553 10.3973 37.8553 9.38505 36.8431L1.83282 29.2908Z" fill="#FB84D3"/>
|
||||
<path d="M22.811 22.1182C22.811 20.6867 23.9715 19.5262 25.403 19.5262H36.0835C37.515 19.5262 38.6755 20.6867 38.6755 22.1182V32.7986C38.6755 34.2302 37.515 35.3906 36.0835 35.3906H25.403C23.9715 35.3906 22.811 34.2302 22.811 32.7986V22.1182Z" fill="#FCB473"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
3
client/assets/images/home/Profile/terms_of_use.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.535 7.5H15.4575C15.681 7.50404 15.8971 7.4196 16.0586 7.26508C16.2201 7.11056 16.3141 6.89847 16.32 6.675V4.9125C16.3102 4.35536 16.1907 3.80561 15.9682 3.29472C15.7457 2.78384 15.4246 2.32185 15.0234 1.93521C14.6221 1.54857 14.1485 1.24487 13.6297 1.0415C13.1109 0.838125 12.5571 0.73907 12 0.750004C10.8773 0.725863 9.79101 1.14831 8.9796 1.92456C8.1682 2.70081 7.69808 3.76738 7.6725 4.89V6.675C7.6784 6.89847 7.77235 7.11056 7.93389 7.26508C8.09544 7.4196 8.31149 7.50404 8.535 7.5ZM12 3C12.2967 3 12.5867 3.08798 12.8334 3.2528C13.08 3.41762 13.2723 3.65189 13.3858 3.92598C13.4994 4.20007 13.5291 4.50167 13.4712 4.79264C13.4133 5.08361 13.2704 5.35089 13.0607 5.56066C12.8509 5.77044 12.5836 5.9133 12.2926 5.97118C12.0017 6.02906 11.7001 5.99935 11.426 5.88582C11.1519 5.77229 10.9176 5.58003 10.7528 5.33336C10.588 5.08669 10.5 4.79668 10.5 4.5C10.5 4.10218 10.658 3.72065 10.9393 3.43934C11.2206 3.15804 11.6022 3 12 3ZM18.96 3.75H17.73C17.7929 4.08894 17.8255 4.43279 17.8275 4.7775V6.615C17.8295 6.92687 17.7699 7.23605 17.6521 7.52484C17.5344 7.81362 17.3608 8.07633 17.1413 8.2979C16.9218 8.51946 16.6608 8.69554 16.3731 8.81602C16.0855 8.9365 15.7769 8.99902 15.465 9H8.535C8.22313 8.99902 7.91453 8.9365 7.62687 8.81602C7.33921 8.69554 7.07816 8.51946 6.85869 8.2979C6.63921 8.07633 6.46562 7.81362 6.34787 7.52484C6.23012 7.23605 6.17052 6.92687 6.1725 6.615V4.7775C6.17451 4.43279 6.20713 4.08894 6.27 3.75H5.04C4.30065 3.75198 3.59216 4.04656 3.06936 4.56936C2.54656 5.09216 2.25198 5.80066 2.25 6.54V20.46C2.25198 21.1993 2.54656 21.9078 3.06936 22.4306C3.59216 22.9534 4.30065 23.248 5.04 23.25H18.96C19.6993 23.248 20.4078 22.9534 20.9306 22.4306C21.4534 21.9078 21.748 21.1993 21.75 20.46V6.54C21.748 5.80066 21.4534 5.09216 20.9306 4.56936C20.4078 4.04656 19.6993 3.75198 18.96 3.75ZM18.345 15.5775L16.095 19.3275C15.995 19.4947 15.8532 19.6331 15.6836 19.7291C15.514 19.8251 15.3224 19.8754 15.1275 19.875H14.9625C14.743 19.8416 14.5383 19.7441 14.3741 19.5946C14.21 19.4451 14.0937 19.2504 14.04 19.035L13.29 15.9825L11.6475 19.26C11.5538 19.4462 11.4102 19.6028 11.2328 19.7121C11.0553 19.8215 10.851 19.8794 10.6425 19.8794C10.434 19.8794 10.2297 19.8215 10.0522 19.7121C9.87476 19.6028 9.73118 19.4462 9.6375 19.26L8.25 16.5L7.8225 17.1375C7.6574 17.3861 7.4003 17.559 7.10774 17.6181C6.81518 17.6772 6.51114 17.6176 6.2625 17.4525C6.01386 17.2874 5.84099 17.0303 5.78191 16.7377C5.72284 16.4452 5.7824 16.1411 5.9475 15.8925L7.4475 13.6425C7.55672 13.4785 7.70712 13.346 7.88364 13.2583C8.06016 13.1707 8.2566 13.131 8.45331 13.1431C8.65001 13.1553 8.84008 13.2189 9.00446 13.3276C9.16885 13.4363 9.3018 13.5863 9.39 13.7625L10.635 16.2525L12.63 12.2625C12.7326 12.0561 12.8961 11.8862 13.0984 11.7757C13.3008 11.6653 13.5321 11.6197 13.7612 11.645C13.9903 11.6704 14.2061 11.7655 14.3794 11.9175C14.5527 12.0696 14.675 12.2711 14.73 12.495L15.57 15.8625L16.425 14.4375C16.5857 14.198 16.8322 14.0294 17.1137 13.9664C17.3952 13.9035 17.6901 13.9511 17.9375 14.0993C18.1849 14.2476 18.3659 14.4852 18.4431 14.763C18.5203 15.0409 18.4879 15.3379 18.3525 15.5925L18.345 15.5775Z" fill="#88D081"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
3
client/assets/images/home/hate.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="31" height="27" viewBox="0 0 31 27" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M22.9404 1C24.8155 1.00014 26.5705 1.9505 27.8818 3.47363C29.195 4.9991 30 7.03214 30 9.04199C30 13.5569 26.9126 17.7942 23.3564 20.9912C21.6007 22.5697 19.7822 23.8471 18.2871 24.7246C17.5392 25.1636 16.8845 25.4954 16.3691 25.7139C16.1111 25.8233 15.8994 25.899 15.7363 25.9463C15.5596 25.9975 15.491 26 15.5 26C15.509 26 15.4404 25.9975 15.2637 25.9463C15.1006 25.899 14.8889 25.8233 14.6309 25.7139C14.1155 25.4954 13.4608 25.1636 12.7129 24.7246C11.2178 23.8471 9.39933 22.5697 7.64355 20.9912C4.08744 17.7942 1 13.5569 1 9.04199C1.00002 7.03211 1.80507 4.99924 3.11719 3.47363C4.42755 1.9502 6.17968 1.00028 8.04883 1C9.43076 1.00118 10.7824 1.41686 11.9365 2.19531C12.6919 2.82182 12.7992 3.32987 12.7617 3.7793C12.7119 4.37623 12.4014 5.00624 11.9795 5.88477C11.6098 6.65467 11.1148 7.69198 11.1807 8.78125C11.2547 10.0032 11.9885 11.0311 13.4072 11.8896C14.8045 12.7352 15.2959 13.5032 15.458 14.0742C15.6235 14.6572 15.5065 15.2425 15.2627 15.8506C15.1695 16.0831 15.0936 16.3559 15.0967 16.6367C15.0994 16.8764 15.1792 17.4392 15.7422 17.7148C16.2715 17.9738 16.7483 17.7343 16.9326 17.6172C17.1613 17.4719 17.3486 17.2695 17.498 17.0664C18.1768 16.1437 19.0283 14.8077 19.248 13.3457C19.3612 12.5927 19.3101 11.7868 18.9688 10.9932C18.6274 10.1997 18.0259 9.48831 17.1504 8.87695C16.2794 8.26883 16.0035 7.65418 15.9551 7.10645C15.9019 6.50515 16.1089 5.81308 16.5215 5.08105C16.9283 4.35923 17.4861 3.68595 17.9961 3.15723C18.514 2.62037 18.933 2.28393 19.0254 2.2207C20.1847 1.42776 21.547 1.00336 22.9404 1Z" stroke="#5E2A28" stroke-width="2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
3
client/assets/images/home/like.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="32" height="27" viewBox="0 0 32 27" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M23.0879 1C24.9887 1 26.7592 1.93575 28.0762 3.42188C29.3956 4.91097 30.2001 6.88999 30.2002 8.84082C30.2002 13.2337 27.1054 17.3696 23.5225 20.499C21.7556 22.0422 19.9254 23.2909 18.4199 24.1494C17.6667 24.5789 17.0062 24.904 16.4863 25.1182C16.2262 25.2253 16.0121 25.3001 15.8467 25.3467C15.6728 25.3956 15.5993 25.4002 15.5996 25.4004C15.5928 25.3997 15.5178 25.3932 15.3525 25.3467C15.1871 25.3001 14.973 25.2253 14.7129 25.1182C14.193 24.904 13.5333 24.5788 12.7803 24.1494C11.2748 23.2909 9.44467 22.0423 7.67773 20.499C4.0947 17.3696 1 13.2338 1 8.84082C1.00008 6.89007 1.80381 4.91108 3.12207 3.42188C4.43787 1.93557 6.20587 1.00033 8.10059 1C9.51294 1.00117 10.8927 1.41742 12.0635 2.19434C12.7589 2.65582 13.3618 3.2322 13.8486 3.89355C14.2951 4.50017 15.0032 4.73135 15.5996 4.73145C16.1961 4.73144 16.905 4.5003 17.3516 3.89355C17.8376 3.23336 18.4389 2.65737 19.1328 2.19629C20.3012 1.4199 21.6779 1.00339 23.0879 1Z" stroke="#5E2A28" stroke-width="2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
4
client/assets/images/home/like_filled.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="32" height="27" viewBox="0 0 32 27" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M23.0879 1C24.9887 1 26.7592 1.93575 28.0762 3.42188C29.3956 4.91097 30.2001 6.88999 30.2002 8.84082C30.2002 13.2337 27.1054 17.3696 23.5225 20.499C21.7556 22.0422 19.9254 23.2909 18.4199 24.1494C17.6667 24.5789 17.0062 24.904 16.4863 25.1182C16.2262 25.2253 16.0121 25.3001 15.8467 25.3467C15.6728 25.3956 15.5993 25.4002 15.5996 25.4004C15.5928 25.3997 15.5178 25.3932 15.3525 25.3467C15.1871 25.3001 14.973 25.2253 14.7129 25.1182C14.193 24.904 13.5333 24.5788 12.7803 24.1494C11.2748 23.2909 9.44467 22.0423 7.67773 20.499C4.0947 17.3696 1 13.2338 1 8.84082C1.00008 6.89007 1.80381 4.91108 3.12207 3.42188C4.43787 1.93557 6.20587 1.00033 8.10059 1C9.51294 1.00117 10.8927 1.41742 12.0635 2.19434C12.7589 2.65582 13.3618 3.2322 13.8486 3.89355C14.2951 4.50017 15.0032 4.73135 15.5996 4.73145C16.1961 4.73144 16.905 4.5003 17.3516 3.89355C17.8376 3.23336 18.4389 2.65737 19.1328 2.19629C20.3012 1.4199 21.6779 1.00339 23.0879 1Z" fill="#5E2A28" stroke="#5E2A28" stroke-width="2"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
4
client/assets/images/home/my.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="22" height="23" viewBox="0 0 22 23" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="10.1829" cy="5.57724" r="4.57724" stroke="#BA906B" stroke-width="2"/>
|
||||
<path d="M11.7158 11.0734C16.3604 11.0734 20.126 14.8389 20.126 19.4836C20.1258 20.8782 18.9952 22.0089 17.6006 22.0089H3.52539C2.13076 22.0089 1.00019 20.8782 1 19.4836C1 14.8389 4.76555 11.0734 9.41016 11.0734H11.7158Z" stroke="#BA906B" stroke-width="2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 443 B |
3
client/assets/images/home/theme.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="19" height="24" viewBox="0 0 19 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.1351 12.3379H17.1918L17.1332 13.3926C17.1331 13.3937 17.1333 13.3964 17.1332 13.3994C17.1329 13.4054 17.1318 13.4148 17.1312 13.4277C17.13 13.4542 17.1282 13.4953 17.1263 13.5488C17.1226 13.656 17.1181 13.8149 17.1156 14.0166C17.1107 14.4207 17.1142 14.9942 17.1469 15.665C17.2127 17.0184 17.3964 18.7184 17.85 20.2178C18.2269 21.4634 17.3822 22.9997 15.8676 23H15.1039L14.972 22.9629C14.2715 22.7667 13.809 22.3377 13.5316 21.8184L13.8011 23H6.39294L6.45154 22.1934C6.39547 22.2908 6.33611 22.3901 6.27771 22.4932L5.9906 23H3.06873C1.55413 22.9997 0.709401 21.4634 1.0863 20.2178C1.53991 18.7184 1.72354 17.0184 1.78943 15.665C1.82209 14.9942 1.8256 14.4207 1.82068 14.0166C1.81822 13.8149 1.81372 13.656 1.80994 13.5488C1.80805 13.4953 1.80626 13.4542 1.80505 13.4277C1.80447 13.4148 1.8034 13.4054 1.8031 13.3994V13.3926L1.74451 12.3379H1.80115V10.0371C1.8066 9.42371 2.04858 8.83549 2.47595 8.39551C2.90321 7.95575 3.48344 7.69746 4.09607 7.67383L4.1156 7.67285H6.41345L5.84021 5.68457L5.83826 5.67773C5.70608 5.20541 5.67049 4.7111 5.73279 4.22461C5.79516 3.73804 5.9545 3.26855 6.20154 2.84473C6.44862 2.42087 6.77857 2.05096 7.17126 1.75684C7.55581 1.46883 7.99319 1.25996 8.45837 1.14062C8.46179 1.13967 8.4657 1.13864 8.46912 1.1377C8.47537 1.13611 8.4814 1.13339 8.48767 1.13184V1.13281C9.42856 0.877513 10.432 0.994629 11.2885 1.46191C12.0997 1.90466 12.7196 2.62807 13.0326 3.49316L13.0902 3.66797L13.0931 3.67773L13.0961 3.68652C13.2814 4.33445 13.2814 5.02199 13.0961 5.66992L13.0951 5.67188L12.5179 7.67285H14.8207L14.8402 7.67383C15.4528 7.69746 16.0331 7.95575 16.4603 8.39551C16.8877 8.83549 17.1297 9.42372 17.1351 10.0371V12.3379Z" stroke="#BA906B" stroke-width="2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
BIN
client/assets/images/index/index_flowers.png
Normal file
|
After Width: | Height: | Size: 212 KiB |
187
client/components/home/DailyReminderModal.tsx
Normal file
@@ -0,0 +1,187 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Pressable, StyleSheet, Switch, Text, View } from 'react-native';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import SheetModal from '@/components/ui/SheetModal';
|
||||
import RemindIcon from '@/assets/images/home/Profile/remind.svg';
|
||||
import {
|
||||
getDailyReminderSettings,
|
||||
setDailyReminderSettings,
|
||||
type DailyReminderSettings,
|
||||
} from '@/src/storage/appStorage';
|
||||
|
||||
type Props = {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export default function DailyReminderModal({ visible, onClose }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [timesPerDay, setTimesPerDay] = useState(3);
|
||||
const [pushEnabled, setPushEnabled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
if (!visible) return;
|
||||
setLoading(true);
|
||||
(async () => {
|
||||
const s = await getDailyReminderSettings();
|
||||
if (cancelled) return;
|
||||
setTimesPerDay(s.timesPerDay);
|
||||
setPushEnabled(s.pushEnabled);
|
||||
setLoading(false);
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [visible]);
|
||||
|
||||
function clamp(next: number) {
|
||||
return Math.min(10, Math.max(1, next));
|
||||
}
|
||||
|
||||
async function onOk() {
|
||||
if (loading) return;
|
||||
setLoading(true);
|
||||
const next: DailyReminderSettings = { timesPerDay, pushEnabled };
|
||||
await setDailyReminderSettings(next);
|
||||
setLoading(false);
|
||||
onClose();
|
||||
}
|
||||
|
||||
return (
|
||||
<SheetModal visible={visible} title={t('dailyReminder.title')} onClose={onClose}>
|
||||
<View style={styles.counter}>
|
||||
<Pressable
|
||||
onPress={() => setTimesPerDay((v) => clamp(v - 1))}
|
||||
hitSlop={10}
|
||||
style={styles.circleBtn}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={t('dailyReminder.minus')}
|
||||
>
|
||||
<Text style={styles.circleText}>-</Text>
|
||||
</Pressable>
|
||||
|
||||
<View style={styles.countCenter}>
|
||||
<Text style={styles.countNumber}>{timesPerDay}</Text>
|
||||
<Text style={styles.countUnit}>{t('dailyReminder.timesUnit')}</Text>
|
||||
</View>
|
||||
|
||||
<Pressable
|
||||
onPress={() => setTimesPerDay((v) => clamp(v + 1))}
|
||||
hitSlop={10}
|
||||
style={styles.circleBtn}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={t('dailyReminder.plus')}
|
||||
>
|
||||
<Text style={styles.circleText}>+</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<View style={styles.row}>
|
||||
<View style={styles.rowLeft}>
|
||||
<View style={styles.rowIcon}>
|
||||
<RemindIcon width={18} height={18} />
|
||||
</View>
|
||||
<Text style={styles.rowText}>{t('dailyReminder.pushLabel')}</Text>
|
||||
</View>
|
||||
<Switch value={pushEnabled} onValueChange={setPushEnabled} />
|
||||
</View>
|
||||
|
||||
<Pressable onPress={onOk} disabled={loading} style={styles.okPressable}>
|
||||
<LinearGradient
|
||||
colors={['#F69F7B', '#F99CC0']}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0 }}
|
||||
style={[styles.okBtn, loading && styles.okBtnDisabled]}
|
||||
>
|
||||
<Text style={styles.okText}>{t('dailyReminder.ok')}</Text>
|
||||
</LinearGradient>
|
||||
</Pressable>
|
||||
</SheetModal>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
counter: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 22,
|
||||
paddingTop: 8,
|
||||
paddingBottom: 22,
|
||||
},
|
||||
circleBtn: {
|
||||
width: 42,
|
||||
height: 42,
|
||||
borderRadius: 21,
|
||||
backgroundColor: 'rgba(244,214,194,0.75)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
circleText: {
|
||||
color: '#5E2A28',
|
||||
fontSize: 20,
|
||||
fontWeight: '700',
|
||||
marginTop: -1,
|
||||
},
|
||||
countCenter: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-end',
|
||||
gap: 6,
|
||||
},
|
||||
countNumber: {
|
||||
color: '#111',
|
||||
fontSize: 54,
|
||||
fontWeight: '800',
|
||||
letterSpacing: -1,
|
||||
},
|
||||
countUnit: {
|
||||
color: '#111',
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
marginBottom: 10,
|
||||
},
|
||||
row: {
|
||||
height: 54,
|
||||
borderRadius: 16,
|
||||
backgroundColor: 'rgba(255,255,255,0.75)',
|
||||
paddingHorizontal: 14,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 18,
|
||||
},
|
||||
rowLeft: { flexDirection: 'row', alignItems: 'center', gap: 10 },
|
||||
rowIcon: {
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 10,
|
||||
backgroundColor: 'rgba(244,214,194,0.55)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
rowText: {
|
||||
color: '#5E2A28',
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
},
|
||||
okPressable: {
|
||||
marginBottom: 6,
|
||||
},
|
||||
okBtn: {
|
||||
height: 52,
|
||||
borderRadius: 26,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
okBtnDisabled: { opacity: 0.7 },
|
||||
okText: {
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
},
|
||||
});
|
||||
|
||||
89
client/components/home/FavoritesModal.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { FlatList, StyleSheet, Text, View } from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import SheetModal from '@/components/ui/SheetModal';
|
||||
import { MOCK_CONTENT } from '@/src/constants/mockContent';
|
||||
import { getFavorites } from '@/src/storage/appStorage';
|
||||
|
||||
type Props = {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export default function FavoritesModal({ visible, onClose }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const [ids, setIds] = useState<string[]>([]);
|
||||
|
||||
// 每次打开时刷新一次,确保展示最新“喜欢”
|
||||
useEffect(() => {
|
||||
if (!visible) return;
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
const list = await getFavorites();
|
||||
if (!cancelled) setIds(list);
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [visible]);
|
||||
|
||||
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]);
|
||||
|
||||
return (
|
||||
<SheetModal visible={visible} title={t('profile.favorites')} onClose={onClose}>
|
||||
<View style={styles.container}>
|
||||
{items.length === 0 ? (
|
||||
<Text style={styles.empty}>{t('favorites.empty')}</Text>
|
||||
) : (
|
||||
<FlatList
|
||||
data={items}
|
||||
keyExtractor={(it) => it.id}
|
||||
contentContainerStyle={styles.list}
|
||||
renderItem={({ item }) => (
|
||||
<View style={styles.row}>
|
||||
<Text style={styles.text}>{item.text}</Text>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</SheetModal>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
borderRadius: 16,
|
||||
backgroundColor: 'rgba(255,255,255,0.75)',
|
||||
overflow: 'hidden',
|
||||
marginBottom: 8,
|
||||
},
|
||||
empty: {
|
||||
color: 'rgba(94,42,40,0.55)',
|
||||
fontSize: 15,
|
||||
textAlign: 'center',
|
||||
paddingVertical: 26,
|
||||
paddingHorizontal: 14,
|
||||
},
|
||||
list: {
|
||||
paddingBottom: 10,
|
||||
},
|
||||
row: {
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 14,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: 'rgba(94,42,40,0.10)',
|
||||
backgroundColor: 'rgba(255,255,255,0.15)',
|
||||
},
|
||||
text: {
|
||||
color: '#5E2A28',
|
||||
fontSize: 15,
|
||||
lineHeight: 22,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
|
||||
662
client/components/home/ProfileModal.tsx
Normal file
@@ -0,0 +1,662 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Alert, FlatList, Image, Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { Switch } from 'react-native';
|
||||
import Animated, {
|
||||
Easing,
|
||||
FadeIn,
|
||||
FadeOut,
|
||||
SlideInLeft,
|
||||
SlideInRight,
|
||||
SlideOutLeft,
|
||||
SlideOutRight,
|
||||
} from 'react-native-reanimated';
|
||||
|
||||
import SheetModal from '@/components/ui/SheetModal';
|
||||
|
||||
import { MOCK_CONTENT } from '@/src/constants/mockContent';
|
||||
import {
|
||||
getDailyReminderSettings,
|
||||
getFavorites,
|
||||
setDailyReminderSettings,
|
||||
type DailyReminderSettings,
|
||||
} 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';
|
||||
|
||||
type Props = {
|
||||
visible: boolean;
|
||||
name?: string;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
type Page = 'root' | 'favorites' | 'dailyReminder' | 'widget';
|
||||
type NavDirection = 'forward' | 'back';
|
||||
|
||||
export default function ProfileModal({ visible, name, onClose }: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [page, setPage] = useState<Page>('root');
|
||||
const [navDirection, setNavDirection] = useState<NavDirection>('forward');
|
||||
const isRoot = page === 'root';
|
||||
|
||||
// 关闭弹窗时重置为首页,避免下次打开停留在二级页
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
setNavDirection('back');
|
||||
setPage('root');
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
function go(next: Page, direction: NavDirection) {
|
||||
setNavDirection(direction);
|
||||
setPage(next);
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
setNavDirection('back');
|
||||
setPage('root');
|
||||
onClose();
|
||||
}
|
||||
|
||||
function handleSheetClose() {
|
||||
// 需求:二级页点击 X 等同于点击“返回”
|
||||
if (!isRoot) {
|
||||
go('root', 'back');
|
||||
return;
|
||||
}
|
||||
handleClose();
|
||||
}
|
||||
|
||||
const title = useMemo(() => {
|
||||
if (page === 'favorites') return t('profile.favorites');
|
||||
if (page === 'dailyReminder') return t('dailyReminder.title');
|
||||
if (page === 'widget') return t('profile.widget');
|
||||
return t('profile.title');
|
||||
}, [page, t]);
|
||||
|
||||
const transition = useMemo(() => {
|
||||
const duration = 220;
|
||||
const easing = Easing.out(Easing.cubic);
|
||||
|
||||
// 进入二级页:从右侧滑入;返回:从左侧滑入
|
||||
const entering =
|
||||
navDirection === 'forward'
|
||||
? SlideInRight.duration(duration).easing(easing)
|
||||
: SlideInLeft.duration(duration).easing(easing);
|
||||
|
||||
// 离开:进入二级页时旧页面向左滑出;返回时旧页面向右滑出
|
||||
const exiting =
|
||||
navDirection === 'forward'
|
||||
? SlideOutLeft.duration(duration).easing(easing)
|
||||
: SlideOutRight.duration(duration).easing(easing);
|
||||
|
||||
return {
|
||||
entering,
|
||||
exiting,
|
||||
fadeIn: FadeIn.duration(duration).easing(easing),
|
||||
fadeOut: FadeOut.duration(duration).easing(easing),
|
||||
};
|
||||
}, [navDirection]);
|
||||
|
||||
return (
|
||||
<SheetModal visible={visible} title={title} onClose={handleSheetClose}>
|
||||
{!isRoot && (
|
||||
<Pressable
|
||||
onPress={() => go('root', 'back')}
|
||||
hitSlop={8}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={t('common.back')}
|
||||
style={styles.backRow}
|
||||
>
|
||||
<Text style={styles.backText}>‹ {t('common.back')}</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
|
||||
<Animated.View
|
||||
key={page}
|
||||
entering={transition.entering}
|
||||
exiting={transition.exiting}
|
||||
style={styles.pageWrap}
|
||||
>
|
||||
<Animated.View entering={transition.fadeIn} exiting={transition.fadeOut}>
|
||||
{page === 'root' ? (
|
||||
<RootPage
|
||||
name={name}
|
||||
onOpenFavorites={() => go('favorites', 'forward')}
|
||||
onOpenWidget={() => go('widget', 'forward')}
|
||||
onOpenDailyReminder={() => go('dailyReminder', 'forward')}
|
||||
/>
|
||||
) : page === 'favorites' ? (
|
||||
<FavoritesPage visible={visible} />
|
||||
) : page === 'dailyReminder' ? (
|
||||
<DailyReminderPage visible={visible} onDone={() => go('root', 'back')} />
|
||||
) : (
|
||||
<WidgetPage />
|
||||
)}
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
</SheetModal>
|
||||
);
|
||||
}
|
||||
|
||||
function toastTodo(t: (key: string) => string) {
|
||||
Alert.alert(t('profile.todoTitle'), t('profile.todoDesc'));
|
||||
}
|
||||
|
||||
function RootPage({
|
||||
name,
|
||||
onOpenFavorites,
|
||||
onOpenWidget,
|
||||
onOpenDailyReminder,
|
||||
}: {
|
||||
name?: string;
|
||||
onOpenFavorites: () => void;
|
||||
onOpenWidget: () => void;
|
||||
onOpenDailyReminder: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<View style={styles.header}>
|
||||
<Image source={AvatarIcon} style={styles.avatarImage} resizeMode="contain" />
|
||||
<Text style={styles.name}>{name || 'Hali'}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.quickRow}>
|
||||
<QuickCard title={t('profile.favorites')} onPress={onOpenFavorites}>
|
||||
<MyLikeIcon width={34} height={34} />
|
||||
</QuickCard>
|
||||
<QuickCard title={t('profile.widget')} onPress={onOpenWidget}>
|
||||
<SmallComponentIcon width={34} height={34} />
|
||||
</QuickCard>
|
||||
</View>
|
||||
|
||||
<View style={styles.list}>
|
||||
<ListItem
|
||||
icon={<RemindIcon width={22} height={22} />}
|
||||
title={t('profile.dailyReminder')}
|
||||
onPress={onOpenDailyReminder}
|
||||
/>
|
||||
<ListItem
|
||||
icon={<PrivacyIcon width={22} height={22} />}
|
||||
title={t('profile.privacy')}
|
||||
onPress={() => toastTodo(t)}
|
||||
/>
|
||||
<ListItem
|
||||
icon={<TermsIcon width={22} height={22} />}
|
||||
title={t('profile.terms')}
|
||||
onPress={() => toastTodo(t)}
|
||||
/>
|
||||
<ListItem
|
||||
icon={<LanguageIcon width={22} height={22} />}
|
||||
title={t('profile.language')}
|
||||
onPress={() => toastTodo(t)}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function FavoritesPage({ visible }: { visible: boolean }) {
|
||||
const { t } = useTranslation();
|
||||
const [ids, setIds] = useState<string[]>([]);
|
||||
|
||||
// 每次进入该页刷新一次,确保展示最新“喜欢”
|
||||
useEffect(() => {
|
||||
if (!visible) return;
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
const list = await getFavorites();
|
||||
if (!cancelled) setIds(list);
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [visible]);
|
||||
|
||||
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]);
|
||||
|
||||
return (
|
||||
<View style={styles.favContainer}>
|
||||
{items.length === 0 ? (
|
||||
<Text style={styles.favEmpty}>{t('favorites.empty')}</Text>
|
||||
) : (
|
||||
<FlatList
|
||||
data={items}
|
||||
keyExtractor={(it) => it.id}
|
||||
contentContainerStyle={styles.favList}
|
||||
renderItem={({ item }) => (
|
||||
<View style={styles.favRow}>
|
||||
<Text style={styles.favText}>{item.text}</Text>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function DailyReminderPage({ visible, onDone }: { visible: boolean; onDone: () => void }) {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [timesPerDay, setTimesPerDay] = useState(3);
|
||||
const [pushEnabled, setPushEnabled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
if (!visible) return;
|
||||
setLoading(true);
|
||||
(async () => {
|
||||
const s = await getDailyReminderSettings();
|
||||
if (cancelled) return;
|
||||
setTimesPerDay(s.timesPerDay);
|
||||
setPushEnabled(s.pushEnabled);
|
||||
setLoading(false);
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [visible]);
|
||||
|
||||
function clamp(next: number) {
|
||||
return Math.min(10, Math.max(1, next));
|
||||
}
|
||||
|
||||
async function onOk() {
|
||||
if (loading) return;
|
||||
setLoading(true);
|
||||
const next: DailyReminderSettings = { timesPerDay, pushEnabled };
|
||||
await setDailyReminderSettings(next);
|
||||
setLoading(false);
|
||||
onDone();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={styles.counter}>
|
||||
<Pressable
|
||||
onPress={() => setTimesPerDay((v) => clamp(v - 1))}
|
||||
hitSlop={10}
|
||||
style={styles.circleBtn}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={t('dailyReminder.minus')}
|
||||
>
|
||||
<Text style={styles.circleText}>-</Text>
|
||||
</Pressable>
|
||||
|
||||
<View style={styles.countCenter}>
|
||||
<Text style={styles.countNumber}>{timesPerDay}</Text>
|
||||
<Text style={styles.countUnit}>{t('dailyReminder.timesUnit')}</Text>
|
||||
</View>
|
||||
|
||||
<Pressable
|
||||
onPress={() => setTimesPerDay((v) => clamp(v + 1))}
|
||||
hitSlop={10}
|
||||
style={styles.circleBtn}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={t('dailyReminder.plus')}
|
||||
>
|
||||
<Text style={styles.circleText}>+</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<View style={styles.remindRow}>
|
||||
<View style={styles.rowLeft}>
|
||||
<View style={styles.rowIcon}>
|
||||
<RemindIcon width={18} height={18} />
|
||||
</View>
|
||||
<Text style={styles.rowText}>{t('dailyReminder.pushLabel')}</Text>
|
||||
</View>
|
||||
<Switch value={pushEnabled} onValueChange={setPushEnabled} />
|
||||
</View>
|
||||
|
||||
<Pressable onPress={onOk} disabled={loading} style={styles.okPressable}>
|
||||
<LinearGradient
|
||||
colors={['#F69F7B', '#F99CC0']}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0 }}
|
||||
style={[styles.okBtn, loading && styles.okBtnDisabled]}
|
||||
>
|
||||
<Text style={styles.okText}>{t('dailyReminder.ok')}</Text>
|
||||
</LinearGradient>
|
||||
</Pressable>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function WidgetPage() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<View style={styles.widgetContent}>
|
||||
<View style={styles.widgetRow}>
|
||||
<View style={styles.widgetCard}>
|
||||
<View style={[styles.widgetPreviewInner, styles.widgetPreviewLock]}>
|
||||
<Text style={styles.widgetPreviewDate} numberOfLines={1}>
|
||||
{t('widget.previewDate')}
|
||||
</Text>
|
||||
<Text style={styles.widgetPreviewTime} numberOfLines={1}>
|
||||
13:45
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={styles.widgetCardLabel}>{t('widget.lockScreen')}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.widgetCard}>
|
||||
<View style={[styles.widgetPreviewInner, styles.widgetPreviewHome]}>
|
||||
<Text style={styles.widgetPreviewQuote} numberOfLines={3}>
|
||||
{t('widget.previewQuote')}
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={styles.widgetCardLabel}>{t('widget.homeScreen')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text style={styles.widgetDesc}>{t('settings.widgetDesc')}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function QuickCard({
|
||||
title,
|
||||
onPress,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
onPress: () => void;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<Pressable onPress={onPress} style={styles.quickCard} hitSlop={6}>
|
||||
<Text style={styles.quickTitle}>{title}</Text>
|
||||
<View style={styles.quickIconWrap}>{children}</View>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
function ListItem({
|
||||
icon,
|
||||
title,
|
||||
onPress,
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
onPress: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Pressable onPress={onPress} style={styles.item} hitSlop={6}>
|
||||
<View style={styles.itemLeft}>
|
||||
<View style={styles.itemIcon}>{icon}</View>
|
||||
<Text style={styles.itemText}>{title}</Text>
|
||||
</View>
|
||||
<Text style={styles.chevron}>›</Text>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
pageWrap: {
|
||||
// 给页面切换动画一个稳定的容器,避免布局抖动
|
||||
},
|
||||
backRow: {
|
||||
alignSelf: 'flex-start',
|
||||
paddingVertical: 4,
|
||||
paddingHorizontal: 2,
|
||||
marginBottom: 6,
|
||||
},
|
||||
backText: {
|
||||
color: 'rgba(94,42,40,0.75)',
|
||||
fontSize: 13,
|
||||
fontWeight: '700',
|
||||
},
|
||||
header: {
|
||||
alignItems: 'center',
|
||||
paddingTop: 6,
|
||||
paddingBottom: 14,
|
||||
},
|
||||
// 头像是默认图片:234x183,没有圆角/边框/背景颜色
|
||||
avatarImage: {
|
||||
width: 234,
|
||||
height: 183,
|
||||
marginBottom: 10,
|
||||
},
|
||||
name: {
|
||||
color: '#5E2A28',
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
},
|
||||
quickRow: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
paddingBottom: 14,
|
||||
},
|
||||
quickCard: {
|
||||
flex: 1,
|
||||
borderRadius: 16,
|
||||
padding: 14,
|
||||
backgroundColor: 'rgba(244,214,194,0.65)',
|
||||
},
|
||||
quickTitle: {
|
||||
color: '#5E2A28',
|
||||
fontSize: 14,
|
||||
fontWeight: '700',
|
||||
marginBottom: 10,
|
||||
},
|
||||
quickIconWrap: { alignItems: 'center', justifyContent: 'center', flex: 1 },
|
||||
list: {
|
||||
borderRadius: 16,
|
||||
backgroundColor: 'rgba(255,255,255,0.75)',
|
||||
overflow: 'hidden',
|
||||
marginBottom: 8,
|
||||
},
|
||||
item: {
|
||||
height: 52,
|
||||
paddingHorizontal: 14,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: 'rgba(94,42,40,0.10)',
|
||||
},
|
||||
itemLeft: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
},
|
||||
itemIcon: {
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 10,
|
||||
backgroundColor: 'rgba(244,214,194,0.55)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
itemText: {
|
||||
color: '#5E2A28',
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
},
|
||||
chevron: {
|
||||
color: 'rgba(94,42,40,0.45)',
|
||||
fontSize: 20,
|
||||
marginTop: -2,
|
||||
},
|
||||
|
||||
favContainer: {
|
||||
borderRadius: 16,
|
||||
backgroundColor: 'rgba(255,255,255,0.75)',
|
||||
overflow: 'hidden',
|
||||
marginBottom: 8,
|
||||
},
|
||||
favEmpty: {
|
||||
color: 'rgba(94,42,40,0.55)',
|
||||
fontSize: 15,
|
||||
textAlign: 'center',
|
||||
paddingVertical: 26,
|
||||
paddingHorizontal: 14,
|
||||
},
|
||||
favList: {
|
||||
paddingBottom: 10,
|
||||
},
|
||||
favRow: {
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 14,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: 'rgba(94,42,40,0.10)',
|
||||
backgroundColor: 'rgba(255,255,255,0.15)',
|
||||
},
|
||||
favText: {
|
||||
color: '#5E2A28',
|
||||
fontSize: 15,
|
||||
lineHeight: 22,
|
||||
fontWeight: '600',
|
||||
},
|
||||
|
||||
counter: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 22,
|
||||
paddingTop: 8,
|
||||
paddingBottom: 22,
|
||||
},
|
||||
circleBtn: {
|
||||
width: 42,
|
||||
height: 42,
|
||||
borderRadius: 21,
|
||||
backgroundColor: 'rgba(244,214,194,0.75)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
circleText: {
|
||||
color: '#5E2A28',
|
||||
fontSize: 20,
|
||||
fontWeight: '700',
|
||||
marginTop: -1,
|
||||
},
|
||||
countCenter: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-end',
|
||||
gap: 6,
|
||||
},
|
||||
countNumber: {
|
||||
color: '#111',
|
||||
fontSize: 54,
|
||||
fontWeight: '800',
|
||||
letterSpacing: -1,
|
||||
},
|
||||
countUnit: {
|
||||
color: '#111',
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
marginBottom: 10,
|
||||
},
|
||||
remindRow: {
|
||||
height: 54,
|
||||
borderRadius: 16,
|
||||
backgroundColor: 'rgba(255,255,255,0.75)',
|
||||
paddingHorizontal: 14,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 18,
|
||||
},
|
||||
rowLeft: { flexDirection: 'row', alignItems: 'center', gap: 10 },
|
||||
rowIcon: {
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 10,
|
||||
backgroundColor: 'rgba(244,214,194,0.55)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
rowText: {
|
||||
color: '#5E2A28',
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
},
|
||||
okPressable: {
|
||||
marginBottom: 6,
|
||||
},
|
||||
okBtn: {
|
||||
height: 52,
|
||||
borderRadius: 26,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
okBtnDisabled: { opacity: 0.7 },
|
||||
okText: {
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
},
|
||||
|
||||
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)',
|
||||
},
|
||||
widgetPreviewInner: {
|
||||
height: 120,
|
||||
padding: 12,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
widgetPreviewLock: {
|
||||
backgroundColor: 'rgba(244,214,194,0.65)',
|
||||
},
|
||||
widgetPreviewHome: {
|
||||
backgroundColor: 'rgba(243,208,225,0.55)',
|
||||
},
|
||||
widgetPreviewDate: {
|
||||
color: 'rgba(94,42,40,0.70)',
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
marginBottom: 6,
|
||||
},
|
||||
widgetPreviewTime: {
|
||||
color: '#ffffff',
|
||||
fontSize: 44,
|
||||
fontWeight: '800',
|
||||
letterSpacing: 1,
|
||||
},
|
||||
widgetPreviewQuote: {
|
||||
color: '#5E2A28',
|
||||
fontSize: 14,
|
||||
lineHeight: 20,
|
||||
fontWeight: '700',
|
||||
},
|
||||
widgetCardLabel: {
|
||||
paddingVertical: 10,
|
||||
textAlign: 'center',
|
||||
color: '#5E2A28',
|
||||
fontSize: 13,
|
||||
fontWeight: '700',
|
||||
},
|
||||
widgetDesc: {
|
||||
color: 'rgba(94,42,40,0.75)',
|
||||
fontSize: 13,
|
||||
lineHeight: 18,
|
||||
},
|
||||
});
|
||||
|
||||
110
client/components/home/ThemeModal.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import React from 'react';
|
||||
import { Image, Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import SheetModal from '@/components/ui/SheetModal';
|
||||
|
||||
export type ThemeMode = 'scenery' | 'color';
|
||||
|
||||
type Props = {
|
||||
visible: boolean;
|
||||
mode: ThemeMode;
|
||||
onSelect: (mode: ThemeMode) => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export default function ThemeModal({ visible, mode, onSelect, onClose }: Props) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<SheetModal visible={visible} title={t('theme.title')} onClose={onClose}>
|
||||
<View style={styles.row}>
|
||||
<ThemeCard
|
||||
title={t('theme.scenery')}
|
||||
selected={mode === 'scenery'}
|
||||
onPress={() => onSelect('scenery')}
|
||||
>
|
||||
<Image
|
||||
source={require('../../assets/images/index/index_flowers.png')}
|
||||
resizeMode="cover"
|
||||
style={styles.previewImage}
|
||||
/>
|
||||
</ThemeCard>
|
||||
|
||||
<ThemeCard
|
||||
title={t('theme.color')}
|
||||
selected={mode === 'color'}
|
||||
onPress={() => onSelect('color')}
|
||||
>
|
||||
<View style={styles.colorPreview} />
|
||||
</ThemeCard>
|
||||
</View>
|
||||
</SheetModal>
|
||||
);
|
||||
}
|
||||
|
||||
function ThemeCard({
|
||||
title,
|
||||
selected,
|
||||
onPress,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
selected: boolean;
|
||||
onPress: () => void;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
style={[styles.card, selected ? styles.cardSelected : styles.cardUnselected]}
|
||||
hitSlop={6}
|
||||
>
|
||||
<View style={styles.preview}>{children}</View>
|
||||
<Text style={styles.cardTitle}>{title}</Text>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
gap: 14,
|
||||
paddingBottom: 18,
|
||||
},
|
||||
card: {
|
||||
flex: 1,
|
||||
borderRadius: 18,
|
||||
padding: 10,
|
||||
backgroundColor: 'rgba(255,255,255,0.55)',
|
||||
},
|
||||
cardSelected: {
|
||||
borderWidth: 2,
|
||||
borderColor: '#F99CC0',
|
||||
},
|
||||
cardUnselected: {
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: 'rgba(94,42,40,0.18)',
|
||||
},
|
||||
preview: {
|
||||
height: 96,
|
||||
borderRadius: 14,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: '#fff',
|
||||
marginBottom: 10,
|
||||
},
|
||||
previewImage: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
colorPreview: {
|
||||
flex: 1,
|
||||
backgroundColor: '#F3D0E1',
|
||||
},
|
||||
cardTitle: {
|
||||
color: '#5E2A28',
|
||||
fontSize: 14,
|
||||
fontWeight: '700',
|
||||
textAlign: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
113
client/components/home/WidgetModal.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import SheetModal from '@/components/ui/SheetModal';
|
||||
|
||||
type Props = {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export default function WidgetModal({ visible, onClose }: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SheetModal visible={visible} title={t('profile.widget')} onClose={onClose}>
|
||||
<View style={styles.content}>
|
||||
<View style={styles.row}>
|
||||
<PreviewCard label={t('widget.lockScreen')}>
|
||||
<View style={[styles.previewInner, styles.previewLock]}>
|
||||
<Text style={styles.previewDate} numberOfLines={1}>
|
||||
{t('widget.previewDate')}
|
||||
</Text>
|
||||
<Text style={styles.previewTime} numberOfLines={1}>
|
||||
13:45
|
||||
</Text>
|
||||
</View>
|
||||
</PreviewCard>
|
||||
|
||||
<PreviewCard label={t('widget.homeScreen')}>
|
||||
<View style={[styles.previewInner, styles.previewHome]}>
|
||||
<Text style={styles.previewQuote} numberOfLines={3}>
|
||||
{t('widget.previewQuote')}
|
||||
</Text>
|
||||
</View>
|
||||
</PreviewCard>
|
||||
</View>
|
||||
|
||||
<Text style={styles.desc}>{t('settings.widgetDesc')}</Text>
|
||||
</View>
|
||||
</SheetModal>
|
||||
);
|
||||
}
|
||||
|
||||
function PreviewCard({ label, children }: { label: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<View style={styles.card}>
|
||||
{children}
|
||||
<Text style={styles.cardLabel}>{label}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
content: {
|
||||
paddingBottom: 18,
|
||||
gap: 14,
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
},
|
||||
card: {
|
||||
flex: 1,
|
||||
borderRadius: 16,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: 'rgba(255,255,255,0.75)',
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: 'rgba(94,42,40,0.12)',
|
||||
},
|
||||
previewInner: {
|
||||
height: 120,
|
||||
padding: 12,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
previewLock: {
|
||||
backgroundColor: 'rgba(244,214,194,0.65)',
|
||||
},
|
||||
previewHome: {
|
||||
backgroundColor: 'rgba(243,208,225,0.55)',
|
||||
},
|
||||
previewDate: {
|
||||
color: 'rgba(94,42,40,0.70)',
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
marginBottom: 6,
|
||||
},
|
||||
previewTime: {
|
||||
color: '#ffffff',
|
||||
fontSize: 44,
|
||||
fontWeight: '800',
|
||||
letterSpacing: 1,
|
||||
},
|
||||
previewQuote: {
|
||||
color: '#5E2A28',
|
||||
fontSize: 14,
|
||||
lineHeight: 20,
|
||||
fontWeight: '700',
|
||||
},
|
||||
cardLabel: {
|
||||
paddingVertical: 10,
|
||||
textAlign: 'center',
|
||||
color: '#5E2A28',
|
||||
fontSize: 13,
|
||||
fontWeight: '700',
|
||||
},
|
||||
desc: {
|
||||
color: 'rgba(94,42,40,0.75)',
|
||||
fontSize: 13,
|
||||
lineHeight: 18,
|
||||
},
|
||||
});
|
||||
|
||||
95
client/components/onboarding/IntentSelectionStep.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import React from 'react';
|
||||
import { View, StyleSheet, TouchableOpacity } from 'react-native';
|
||||
import { SerifText } from './SerifText';
|
||||
import { OnboardingColors } from '@/constants/OnboardingTheme';
|
||||
|
||||
export const INTENTS = [
|
||||
{ id: 'love', label: '爱情', icon: '❤️' },
|
||||
{ id: 'life', label: '生活', icon: '⛅' },
|
||||
{ id: 'travel', label: '旅游', icon: '🌴' },
|
||||
{ id: 'work', label: '职场', icon: '💼' },
|
||||
];
|
||||
|
||||
interface IntentSelectionStepProps {
|
||||
selectedIds: string[];
|
||||
onToggle: (id: string) => void;
|
||||
}
|
||||
|
||||
export function IntentSelectionStep({ selectedIds, onToggle }: IntentSelectionStepProps) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<SerifText style={styles.title}>你希望得到什么帮助?</SerifText>
|
||||
|
||||
<View style={styles.grid}>
|
||||
{INTENTS.map((intent) => {
|
||||
const isSelected = selectedIds.includes(intent.id);
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={intent.id}
|
||||
style={[
|
||||
styles.card,
|
||||
isSelected && styles.cardSelected
|
||||
]}
|
||||
onPress={() => onToggle(intent.id)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<SerifText style={styles.icon}>{intent.icon}</SerifText>
|
||||
<SerifText style={[styles.label, isSelected && styles.labelSelected]}>
|
||||
{intent.label}
|
||||
</SerifText>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
marginBottom: 40,
|
||||
textAlign: 'center',
|
||||
},
|
||||
grid: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: 16,
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
},
|
||||
card: {
|
||||
width: '45%', // slightly less than half to accommodate gap
|
||||
aspectRatio: 1.2,
|
||||
backgroundColor: OnboardingColors.cardBackground,
|
||||
borderRadius: 20,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 10,
|
||||
elevation: 2,
|
||||
borderWidth: 2,
|
||||
borderColor: 'transparent',
|
||||
},
|
||||
cardSelected: {
|
||||
borderColor: OnboardingColors.buttonEnd, // Use pink as highlight
|
||||
backgroundColor: '#FFFBF5',
|
||||
},
|
||||
icon: {
|
||||
fontSize: 32,
|
||||
},
|
||||
label: {
|
||||
fontSize: 18,
|
||||
color: OnboardingColors.textPrimary,
|
||||
},
|
||||
labelSelected: {
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
});
|
||||
64
client/components/onboarding/NameInputStep.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import { View, StyleSheet, TextInput, Platform } from 'react-native';
|
||||
import { SerifText } from './SerifText';
|
||||
import { OnboardingColors } from '@/constants/OnboardingTheme';
|
||||
|
||||
interface NameInputStepProps {
|
||||
value: string;
|
||||
onChangeText: (text: string) => void;
|
||||
onSubmitEditing?: () => void;
|
||||
}
|
||||
|
||||
export function NameInputStep({ value, onChangeText, onSubmitEditing }: NameInputStepProps) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<SerifText style={styles.title}>我可以怎么称呼你?</SerifText>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={value}
|
||||
onChangeText={onChangeText}
|
||||
placeholder="Hao"
|
||||
placeholderTextColor={OnboardingColors.textSecondary}
|
||||
selectionColor={OnboardingColors.cursor}
|
||||
autoFocus
|
||||
onSubmitEditing={onSubmitEditing}
|
||||
returnKeyType="done"
|
||||
textAlign="center"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
marginBottom: 40,
|
||||
textAlign: 'center',
|
||||
},
|
||||
inputContainer: {
|
||||
width: '100%',
|
||||
backgroundColor: OnboardingColors.cardBackground,
|
||||
borderRadius: 20,
|
||||
paddingVertical: 20,
|
||||
paddingHorizontal: 24,
|
||||
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' }),
|
||||
color: OnboardingColors.textPrimary,
|
||||
textAlign: 'center',
|
||||
padding: 0, // remove default padding
|
||||
},
|
||||
});
|
||||
48
client/components/onboarding/NextButton.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import { TouchableOpacity, StyleSheet, ViewStyle } from 'react-native';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import Svg, { Path } from 'react-native-svg';
|
||||
import { OnboardingColors } from '@/constants/OnboardingTheme';
|
||||
|
||||
interface NextButtonProps {
|
||||
onPress: () => void;
|
||||
disabled?: boolean;
|
||||
style?: ViewStyle;
|
||||
}
|
||||
|
||||
export function NextButton({ onPress, disabled, style }: NextButtonProps) {
|
||||
return (
|
||||
<TouchableOpacity onPress={onPress} disabled={disabled} activeOpacity={0.8} style={[styles.container, style]}>
|
||||
<LinearGradient
|
||||
colors={[OnboardingColors.buttonStart, OnboardingColors.buttonEnd]}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
style={[styles.gradient, disabled && styles.disabled]}
|
||||
>
|
||||
<Svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<Path d="M5 12h14M12 5l7 7-7 7" />
|
||||
</Svg>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
shadowColor: OnboardingColors.buttonEnd,
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 8,
|
||||
elevation: 5,
|
||||
},
|
||||
gradient: {
|
||||
width: 64,
|
||||
height: 64,
|
||||
borderRadius: 32,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
disabled: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
});
|
||||
84
client/components/onboarding/OnboardingLayout.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import React from 'react';
|
||||
import { View, StyleSheet, SafeAreaView, TouchableOpacity, StatusBar } 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;
|
||||
}
|
||||
|
||||
export function OnboardingLayout({
|
||||
children,
|
||||
onSkip,
|
||||
onNext,
|
||||
nextEnabled = true,
|
||||
showNextButton = true
|
||||
}: OnboardingLayoutProps) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<StatusBar barStyle="dark-content" />
|
||||
<SafeAreaView style={styles.safeArea}>
|
||||
<View style={styles.header}>
|
||||
<View style={styles.spacer} />
|
||||
{onSkip && (
|
||||
<TouchableOpacity onPress={onSkip} style={styles.skipButton}>
|
||||
<SerifText style={styles.skipText}>Skip {'->'}</SerifText>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.content}>
|
||||
{children}
|
||||
</View>
|
||||
|
||||
<View style={styles.footer}>
|
||||
{showNextButton && onNext && (
|
||||
<NextButton onPress={onNext} disabled={!nextEnabled} />
|
||||
)}
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: OnboardingColors.background,
|
||||
},
|
||||
safeArea: {
|
||||
flex: 1,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
spacer: {
|
||||
width: 60, // Balance the skip button width approximately
|
||||
},
|
||||
skipButton: {
|
||||
padding: 8,
|
||||
},
|
||||
skipText: {
|
||||
fontSize: 16,
|
||||
color: OnboardingColors.textPrimary,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 24,
|
||||
},
|
||||
footer: {
|
||||
alignItems: 'center',
|
||||
paddingBottom: 40,
|
||||
minHeight: 100, // Reserve space for button
|
||||
},
|
||||
});
|
||||
30
client/components/onboarding/SerifText.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import { Text, TextProps, Platform, StyleSheet } from 'react-native';
|
||||
import { OnboardingColors } from '@/constants/OnboardingTheme';
|
||||
|
||||
interface SerifTextProps extends TextProps {
|
||||
weight?: 'regular' | 'bold';
|
||||
}
|
||||
|
||||
export function SerifText({ style, weight = 'regular', ...otherProps }: SerifTextProps) {
|
||||
return (
|
||||
<Text
|
||||
style={[
|
||||
styles.text,
|
||||
weight === 'bold' && styles.bold,
|
||||
style,
|
||||
]}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
text: {
|
||||
fontFamily: Platform.select({ ios: 'Georgia', android: 'serif', default: 'serif' }),
|
||||
color: OnboardingColors.textPrimary,
|
||||
},
|
||||
bold: {
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
});
|
||||
138
client/components/ui/SheetModal.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Modal, Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import Animated, {
|
||||
Easing,
|
||||
runOnJS,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withTiming,
|
||||
} from 'react-native-reanimated';
|
||||
|
||||
type Props = {
|
||||
visible: boolean;
|
||||
title?: string;
|
||||
onClose: () => void;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
/**
|
||||
* 通用底部上拉弹窗(Sheet)
|
||||
* - 内容区背景色固定:#FAF3EC
|
||||
* - 关闭方式:点 X(本期不要求点遮罩关闭)
|
||||
*/
|
||||
export default function SheetModal({ visible, title, onClose, children }: Props) {
|
||||
const insets = useSafeAreaInsets();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const progress = useSharedValue(0); // 0: 关闭, 1: 打开
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
setMounted(true);
|
||||
progress.value = withTiming(1, { duration: 260, easing: Easing.out(Easing.cubic) });
|
||||
return;
|
||||
}
|
||||
if (!mounted) return;
|
||||
progress.value = withTiming(
|
||||
0,
|
||||
{ duration: 220, easing: Easing.in(Easing.cubic) },
|
||||
(finished) => {
|
||||
if (finished) runOnJS(setMounted)(false);
|
||||
}
|
||||
);
|
||||
}, [visible, mounted, progress]);
|
||||
|
||||
const overlayStyle = useAnimatedStyle(() => {
|
||||
return { opacity: 0.4 * progress.value };
|
||||
});
|
||||
|
||||
const sheetStyle = useAnimatedStyle(() => {
|
||||
const translateY = (1 - progress.value) * 380;
|
||||
return { transform: [{ translateY }] };
|
||||
});
|
||||
|
||||
const containerPaddingBottom = useMemo(() => Math.max(insets.bottom, 12), [insets.bottom]);
|
||||
|
||||
// 注意:Modal 的 visible 必须为 true 才会渲染,因此用 mounted 保持退场动画
|
||||
return (
|
||||
<Modal transparent visible={mounted} animationType="none" onRequestClose={onClose}>
|
||||
<View style={styles.root}>
|
||||
<Animated.View style={[styles.overlay, overlayStyle]} />
|
||||
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.sheet,
|
||||
sheetStyle,
|
||||
{
|
||||
paddingBottom: containerPaddingBottom,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.title} numberOfLines={1}>
|
||||
{title ?? ''}
|
||||
</Text>
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="关闭"
|
||||
onPress={onClose}
|
||||
hitSlop={10}
|
||||
style={styles.close}
|
||||
>
|
||||
<Text style={styles.closeText}>×</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<View style={styles.body}>{children}</View>
|
||||
</Animated.View>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
root: {
|
||||
flex: 1,
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
overlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: '#000',
|
||||
},
|
||||
sheet: {
|
||||
backgroundColor: '#FAF3EC',
|
||||
borderTopLeftRadius: 24,
|
||||
borderTopRightRadius: 24,
|
||||
paddingTop: 14,
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
header: {
|
||||
height: 44,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
title: {
|
||||
color: '#5E2A28',
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
},
|
||||
close: {
|
||||
position: 'absolute',
|
||||
left: 4,
|
||||
top: 0,
|
||||
height: 44,
|
||||
width: 44,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
closeText: {
|
||||
color: '#5E2A28',
|
||||
fontSize: 28,
|
||||
lineHeight: 28,
|
||||
fontWeight: '400',
|
||||
},
|
||||
body: {
|
||||
paddingTop: 10,
|
||||
},
|
||||
});
|
||||
|
||||
9
client/constants/OnboardingTheme.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export const OnboardingColors = {
|
||||
background: '#FFF4EA',
|
||||
textPrimary: '#4A3B32',
|
||||
textSecondary: '#8C8C8C',
|
||||
buttonStart: '#F69F7B',
|
||||
buttonEnd: '#F99CC0',
|
||||
cardBackground: '#FFFFFF',
|
||||
cursor: '#F99CC0',
|
||||
};
|
||||
@@ -41,6 +41,8 @@ PODS:
|
||||
- RNScreens
|
||||
- ExpoKeepAwake (15.0.8):
|
||||
- ExpoModulesCore
|
||||
- ExpoLinearGradient (15.0.8):
|
||||
- ExpoModulesCore
|
||||
- ExpoLinking (8.0.11):
|
||||
- ExpoModulesCore
|
||||
- ExpoLocalization (17.0.8):
|
||||
@@ -1917,6 +1919,51 @@ PODS:
|
||||
- ReactCommon/turbomodule/core
|
||||
- ReactNativeDependencies
|
||||
- Yoga
|
||||
- RNSVG (15.12.1):
|
||||
- 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
|
||||
- RNSVG/common (= 15.12.1)
|
||||
- Yoga
|
||||
- RNSVG/common (15.12.1):
|
||||
- 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
|
||||
- RNWorklets (0.5.1):
|
||||
- hermes-engine
|
||||
- RCTRequired
|
||||
@@ -2000,6 +2047,7 @@ DEPENDENCIES:
|
||||
- "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.13_expo_rjurfbyy5kjn57nkkfxix5iqea/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+react@_e6k2hjkd5k4lph2ersbp3gfshy/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`)"
|
||||
- "ExpoLocalization (from `../node_modules/.pnpm/expo-localization@17.0.8_expo@54.0.32_react@19.1.0/node_modules/expo-localization/ios`)"
|
||||
- "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`)"
|
||||
@@ -2076,6 +2124,7 @@ DEPENDENCIES:
|
||||
- "RNCAsyncStorage (from `../node_modules/.pnpm/@react-native-async-storage+async-storage@2.2.0_react-native@0.81.5_@babel+core@7.28.6_@types_fp4qq3a7mejmut52v6jrlvxlzi/node_modules/@react-native-async-storage/async-storage`)"
|
||||
- "RNReanimated (from `../node_modules/.pnpm/react-native-reanimated@4.1.6_@babel+core@7.28.6_react-native-worklets@0.5.1_@babel+core@7.28_ky3sbxf6i7nkyacc2hzg3xcz4q/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`)"
|
||||
- "RNWorklets (from `../node_modules/.pnpm/react-native-worklets@0.5.1_@babel+core@7.28.6_react-native@0.81.5_@babel+core@7.28.6_@types+_5atwepuw3zy3crkgvetf35tkve/node_modules/react-native-worklets`)"
|
||||
- "Yoga (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/yoga`)"
|
||||
|
||||
@@ -2098,6 +2147,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/.pnpm/expo-router@6.0.22_@expo+metro-runtime@6.1.2_@types+react@19.1.17_expo-constants@18.0.13_expo_rjurfbyy5kjn57nkkfxix5iqea/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:
|
||||
:path: "../node_modules/.pnpm/expo-linear-gradient@15.0.8_expo@54.0.32_react-native@0.81.5_@babel+core@7.28.6_@types+react@_e6k2hjkd5k4lph2ersbp3gfshy/node_modules/expo-linear-gradient/ios"
|
||||
ExpoLinking:
|
||||
:path: "../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"
|
||||
ExpoLocalization:
|
||||
@@ -2249,6 +2300,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/.pnpm/react-native-reanimated@4.1.6_@babel+core@7.28.6_react-native-worklets@0.5.1_@babel+core@7.28_ky3sbxf6i7nkyacc2hzg3xcz4q/node_modules/react-native-reanimated"
|
||||
RNScreens:
|
||||
:path: "../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:
|
||||
:path: "../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"
|
||||
RNWorklets:
|
||||
:path: "../node_modules/.pnpm/react-native-worklets@0.5.1_@babel+core@7.28.6_react-native@0.81.5_@babel+core@7.28.6_@types+_5atwepuw3zy3crkgvetf35tkve/node_modules/react-native-worklets"
|
||||
Yoga:
|
||||
@@ -2264,6 +2317,7 @@ SPEC CHECKSUMS:
|
||||
ExpoFont: c625dbd97ed57e9089b172b2a7bb99003d074664
|
||||
ExpoHead: b691a2ed7ab02ed820b6c6468941832d34969c29
|
||||
ExpoKeepAwake: 44bf6715bc1d2ddb17afe19d927cd039cda123f0
|
||||
ExpoLinearGradient: 814a21fc4056c3cf606e4f19e31e47074c5b5a86
|
||||
ExpoLinking: ebf543fd411d56375cb4eee07f6ab4e31c7ad959
|
||||
ExpoLocalization: 6ac6f326210f0a3141ef6f58ab8f8f4ed003b485
|
||||
ExpoModulesCore: 77496909fd3c800f97f7f2007dd26aeac4bb3798
|
||||
@@ -2339,6 +2393,7 @@ SPEC CHECKSUMS:
|
||||
RNCAsyncStorage: e85a99325df9eb0191a6ee2b2a842644c7eb29f4
|
||||
RNReanimated: 10415bc8396eaeac0d7b2c9a1538eae7e607ec9c
|
||||
RNScreens: dd61bc3a3e6f6901ad833efa411917d44827cf51
|
||||
RNSVG: 2825ee146e0f6a16221e852299943e4cceef4528
|
||||
RNWorklets: 9ccdc8112b17af6eee2c85a233891cb80db150ad
|
||||
Yoga: 5934998fbeaef7845dbf698f698518695ab4cd1a
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 70;
|
||||
objectVersion = 56;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
@@ -11,7 +11,7 @@
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
||||
1A1DE01D4133812B2E2BA692 /* libPods-client.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E3328F0E595C1F4A244DF238 /* libPods-client.a */; };
|
||||
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
|
||||
A1B2C3D4E5F60718293A4B5C /* 情绪小组件/EmotionWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60718293A4B5B /* 情绪小组件/EmotionWidget.swift */; };
|
||||
A1B2C3D4E5F60718293A4B5C /* EmotionWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60718293A4B5B /* EmotionWidget.swift */; };
|
||||
B5A7FE9A125F7C79753EC5BF /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DB40C26E3A46F6D06769EA /* ExpoModulesProvider.swift */; };
|
||||
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
|
||||
EB3DAF812F2A4B8E00450593 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EB3DAF802F2A4B8D00450593 /* WidgetKit.framework */; };
|
||||
@@ -50,7 +50,7 @@
|
||||
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = client/Info.plist; sourceTree = "<group>"; };
|
||||
3C76CA16D0801CBF0D731C7C /* Pods-client.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-client.release.xcconfig"; path = "Target Support Files/Pods-client/Pods-client.release.xcconfig"; sourceTree = "<group>"; };
|
||||
75F52ADE07CAE9D9736D7671 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = client/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||
A1B2C3D4E5F60718293A4B5B /* 情绪小组件/EmotionWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "情绪小组件/EmotionWidget.swift"; sourceTree = "<group>"; };
|
||||
A1B2C3D4E5F60718293A4B5B /* EmotionWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "情绪小组件/EmotionWidget.swift"; sourceTree = "<group>"; };
|
||||
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = client/SplashScreen.storyboard; sourceTree = "<group>"; };
|
||||
BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = "<group>"; };
|
||||
C7DB40C26E3A46F6D06769EA /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-client/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
|
||||
@@ -66,7 +66,7 @@
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
EB3DAF952F2A4B8F00450593 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
|
||||
EB3DAF952F2A4B8F00450593 /* Exceptions for "情绪小组件" folder in "情绪小组件Extension" target */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
EmotionWidget.swift,
|
||||
@@ -77,7 +77,18 @@
|
||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
EB3DAF842F2A4B8E00450593 /* 情绪小组件 */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (EB3DAF952F2A4B8F00450593 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = "情绪小组件"; sourceTree = "<group>"; };
|
||||
EB3DAF842F2A4B8E00450593 /* 情绪小组件 */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
EB3DAF952F2A4B8F00450593 /* Exceptions for "情绪小组件" folder in "情绪小组件Extension" target */,
|
||||
);
|
||||
explicitFileTypes = {
|
||||
};
|
||||
explicitFolders = (
|
||||
);
|
||||
path = "情绪小组件";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -189,7 +200,7 @@
|
||||
EB3DAFD42F2A5FC100450593 /* Recovered References */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A1B2C3D4E5F60718293A4B5B /* 情绪小组件/EmotionWidget.swift */,
|
||||
A1B2C3D4E5F60718293A4B5B /* EmotionWidget.swift */,
|
||||
);
|
||||
name = "Recovered References";
|
||||
sourceTree = "<group>";
|
||||
@@ -245,8 +256,6 @@
|
||||
EB3DAF842F2A4B8E00450593 /* 情绪小组件 */,
|
||||
);
|
||||
name = "情绪小组件Extension";
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = "情绪小组件Extension";
|
||||
productReference = EB3DAF7F2F2A4B8D00450593 /* 情绪小组件Extension.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
@@ -362,6 +371,7 @@
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoLocalization/ExpoLocalization_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle",
|
||||
"${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",
|
||||
);
|
||||
@@ -374,6 +384,7 @@
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoLocalization_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCAsyncStorage_resources.bundle",
|
||||
"${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",
|
||||
);
|
||||
@@ -444,7 +455,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A1B2C3D4E5F60718293A4B5C /* 情绪小组件/EmotionWidget.swift in Sources */,
|
||||
A1B2C3D4E5F60718293A4B5C /* EmotionWidget.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -683,6 +694,7 @@
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.anonymous.client.emotionwidget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
@@ -733,6 +745,7 @@
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
MTL_FAST_MATH = YES;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.anonymous.client.emotionwidget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
|
||||
@@ -7,5 +7,7 @@
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.widgetkit-extension</string>
|
||||
</dict>
|
||||
<key>RCTNewArchEnabled</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
20
client/metro.config.js
Normal file
@@ -0,0 +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);
|
||||
|
||||
config.transformer = {
|
||||
...config.transformer,
|
||||
babelTransformerPath: require.resolve('react-native-svg-transformer'),
|
||||
};
|
||||
|
||||
config.resolver = {
|
||||
...config.resolver,
|
||||
assetExts: config.resolver.assetExts.filter((ext) => ext !== 'svg'),
|
||||
sourceExts: [...config.resolver.sourceExts, 'svg'],
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"expo": "~54.0.32",
|
||||
"expo-constants": "~18.0.13",
|
||||
"expo-font": "~14.0.11",
|
||||
"expo-linear-gradient": "^15.0.8",
|
||||
"expo-linking": "~8.0.11",
|
||||
"expo-localization": "^17.0.8",
|
||||
"expo-notifications": "^0.32.16",
|
||||
@@ -31,6 +32,8 @@
|
||||
"react-native-reanimated": "~4.1.1",
|
||||
"react-native-safe-area-context": "~5.6.0",
|
||||
"react-native-screens": "~4.16.0",
|
||||
"react-native-svg": "15.12.1",
|
||||
"react-native-svg-transformer": "^1.5.3",
|
||||
"react-native-web": "~0.21.0",
|
||||
"react-native-worklets": "0.5.1"
|
||||
},
|
||||
|
||||
466
client/pnpm-lock.yaml
generated
@@ -26,6 +26,9 @@ importers:
|
||||
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)
|
||||
expo-linear-gradient:
|
||||
specifier: ^15.0.8
|
||||
version: 15.0.8(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-linking:
|
||||
specifier: ~8.0.11
|
||||
version: 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)
|
||||
@@ -71,6 +74,12 @@ importers:
|
||||
react-native-screens:
|
||||
specifier: ~4.16.0
|
||||
version: 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-svg:
|
||||
specifier: 15.12.1
|
||||
version: 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)
|
||||
react-native-svg-transformer:
|
||||
specifier: ^1.5.3
|
||||
version: 1.5.3(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))(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(typescript@5.9.3)
|
||||
react-native-web:
|
||||
specifier: ~0.21.0
|
||||
version: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
@@ -1138,6 +1147,84 @@ packages:
|
||||
'@sinonjs/fake-timers@10.3.0':
|
||||
resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==}
|
||||
|
||||
'@svgr/babel-plugin-add-jsx-attribute@8.0.0':
|
||||
resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==}
|
||||
engines: {node: '>=14'}
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
|
||||
'@svgr/babel-plugin-remove-jsx-attribute@8.0.0':
|
||||
resolution: {integrity: sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==}
|
||||
engines: {node: '>=14'}
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
|
||||
'@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0':
|
||||
resolution: {integrity: sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==}
|
||||
engines: {node: '>=14'}
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
|
||||
'@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0':
|
||||
resolution: {integrity: sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==}
|
||||
engines: {node: '>=14'}
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
|
||||
'@svgr/babel-plugin-svg-dynamic-title@8.0.0':
|
||||
resolution: {integrity: sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==}
|
||||
engines: {node: '>=14'}
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
|
||||
'@svgr/babel-plugin-svg-em-dimensions@8.0.0':
|
||||
resolution: {integrity: sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==}
|
||||
engines: {node: '>=14'}
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
|
||||
'@svgr/babel-plugin-transform-react-native-svg@8.1.0':
|
||||
resolution: {integrity: sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==}
|
||||
engines: {node: '>=14'}
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
|
||||
'@svgr/babel-plugin-transform-svg-component@8.0.0':
|
||||
resolution: {integrity: sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==}
|
||||
engines: {node: '>=12'}
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
|
||||
'@svgr/babel-preset@8.1.0':
|
||||
resolution: {integrity: sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==}
|
||||
engines: {node: '>=14'}
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
|
||||
'@svgr/core@8.1.0':
|
||||
resolution: {integrity: sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@svgr/hast-util-to-babel-ast@8.0.0':
|
||||
resolution: {integrity: sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@svgr/plugin-jsx@8.1.0':
|
||||
resolution: {integrity: sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==}
|
||||
engines: {node: '>=14'}
|
||||
peerDependencies:
|
||||
'@svgr/core': '*'
|
||||
|
||||
'@svgr/plugin-svgo@8.1.0':
|
||||
resolution: {integrity: sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA==}
|
||||
engines: {node: '>=14'}
|
||||
peerDependencies:
|
||||
'@svgr/core': '*'
|
||||
|
||||
'@trysound/sax@0.2.0':
|
||||
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
'@types/babel__core@7.20.5':
|
||||
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
|
||||
|
||||
@@ -1362,6 +1449,9 @@ packages:
|
||||
resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
||||
boolbase@1.0.0:
|
||||
resolution: {integrity: sha1-aN/1++YMUes3cl6p4+0xDcwed24=}
|
||||
|
||||
bplist-creator@0.1.0:
|
||||
resolution: {integrity: sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==}
|
||||
|
||||
@@ -1413,6 +1503,10 @@ packages:
|
||||
resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
callsites@3.1.0:
|
||||
resolution: {integrity: sha1-s2MKvYlDQy9Us/BRkjjjPNffL3M=}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
camelcase@5.3.1:
|
||||
resolution: {integrity: sha1-48mzFWnhBoEd8kL3FXJaH0xJQyA=}
|
||||
engines: {node: '>=6'}
|
||||
@@ -1526,6 +1620,15 @@ packages:
|
||||
core-js-compat@3.48.0:
|
||||
resolution: {integrity: sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==}
|
||||
|
||||
cosmiconfig@8.3.6:
|
||||
resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==}
|
||||
engines: {node: '>=14'}
|
||||
peerDependencies:
|
||||
typescript: '>=4.9.5'
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
cross-fetch@3.2.0:
|
||||
resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==}
|
||||
|
||||
@@ -1540,6 +1643,29 @@ packages:
|
||||
css-in-js-utils@3.1.0:
|
||||
resolution: {integrity: sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==}
|
||||
|
||||
css-select@5.2.2:
|
||||
resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==}
|
||||
|
||||
css-tree@1.1.3:
|
||||
resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
css-tree@2.2.1:
|
||||
resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==}
|
||||
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
|
||||
|
||||
css-tree@2.3.1:
|
||||
resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==}
|
||||
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
|
||||
|
||||
css-what@6.2.2:
|
||||
resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
csso@5.0.5:
|
||||
resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==}
|
||||
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
|
||||
|
||||
csstype@3.2.3:
|
||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||
|
||||
@@ -1610,6 +1736,22 @@ packages:
|
||||
detect-node-es@1.1.0:
|
||||
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
|
||||
|
||||
dom-serializer@2.0.0:
|
||||
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
|
||||
|
||||
domelementtype@2.3.0:
|
||||
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
|
||||
|
||||
domhandler@5.0.3:
|
||||
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
domutils@3.2.2:
|
||||
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
|
||||
|
||||
dot-case@3.0.4:
|
||||
resolution: {integrity: sha1-mytnDQCkMWZ6inW6Kc0bmICc51E=}
|
||||
|
||||
dotenv-expand@11.0.7:
|
||||
resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1639,10 +1781,17 @@ packages:
|
||||
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
entities@4.5.0:
|
||||
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||
engines: {node: '>=0.12'}
|
||||
|
||||
env-editor@0.4.2:
|
||||
resolution: {integrity: sha512-ObFo8v4rQJAE59M69QzwloxPZtd33TpYEIjtKD1rrFDcM1Gd7IkDxEBU+HriziN6HSHQnBJi8Dmy+JWkav5HKA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
error-ex@1.3.4:
|
||||
resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
|
||||
|
||||
error-stack-parser@2.1.4:
|
||||
resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==}
|
||||
|
||||
@@ -1730,6 +1879,13 @@ packages:
|
||||
expo: '*'
|
||||
react: '*'
|
||||
|
||||
expo-linear-gradient@15.0.8:
|
||||
resolution: {integrity: sha512-V2d8Wjn0VzhPHO+rrSBtcl+Fo+jUUccdlmQ6OoL9/XQB7Qk3d9lYrqKDJyccwDxmQT10JdST3Tmf2K52NLc3kw==}
|
||||
peerDependencies:
|
||||
expo: '*'
|
||||
react: '*'
|
||||
react-native: '*'
|
||||
|
||||
expo-linking@8.0.11:
|
||||
resolution: {integrity: sha512-+VSaNL5om3kOp/SSKO5qe6cFgfSIWnnQDSbA7XLs3ECkYzXRquk5unxNS3pg7eK5kNUmQ4kgLI7MhTggAEUBLA==}
|
||||
peerDependencies:
|
||||
@@ -2030,6 +2186,10 @@ packages:
|
||||
engines: {node: '>=16.x'}
|
||||
hasBin: true
|
||||
|
||||
import-fresh@3.3.1:
|
||||
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
imurmurhash@0.1.4:
|
||||
resolution: {integrity: sha1-khi5srkoojixPcT7a21XbyMUU+o=}
|
||||
engines: {node: '>=0.8.19'}
|
||||
@@ -2053,6 +2213,9 @@ packages:
|
||||
resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
is-arrayish@0.2.1:
|
||||
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
|
||||
|
||||
is-arrayish@0.3.4:
|
||||
resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==}
|
||||
|
||||
@@ -2170,6 +2333,9 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
hasBin: true
|
||||
|
||||
json-parse-even-better-errors@2.3.1:
|
||||
resolution: {integrity: sha1-fEeAWpQxmSjgV3dAXcEuH3pO4C0=}
|
||||
|
||||
json5@2.2.3:
|
||||
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -2289,6 +2455,9 @@ packages:
|
||||
resolution: {integrity: sha1-ce5R+nvkyuwaY4OffmgtgTLTDK8=}
|
||||
hasBin: true
|
||||
|
||||
lower-case@2.0.2:
|
||||
resolution: {integrity: sha1-b6I3xj29xKgsoP2ILkci3F5jTig=}
|
||||
|
||||
lru-cache@10.4.3:
|
||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||
|
||||
@@ -2309,6 +2478,15 @@ packages:
|
||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
mdn-data@2.0.14:
|
||||
resolution: {integrity: sha1-cRP8QoGRfWPOKbQ0RvcB5owlulA=}
|
||||
|
||||
mdn-data@2.0.28:
|
||||
resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==}
|
||||
|
||||
mdn-data@2.0.30:
|
||||
resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
|
||||
|
||||
memoize-one@5.2.1:
|
||||
resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==}
|
||||
|
||||
@@ -2457,6 +2635,9 @@ packages:
|
||||
nested-error-stacks@2.0.1:
|
||||
resolution: {integrity: sha1-0syfxSNd2zcfxE1QYjQznI5LCks=}
|
||||
|
||||
no-case@3.0.4:
|
||||
resolution: {integrity: sha1-02H9XJgA9VhVGoNp/A3NRmK2Ek0=}
|
||||
|
||||
node-fetch@2.7.0:
|
||||
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
||||
engines: {node: 4.x || >=6.0.0}
|
||||
@@ -2484,6 +2665,9 @@ packages:
|
||||
resolution: {integrity: sha512-sHGJy8sOC1YraBywpzQlIKBE4pBbGbiF95U6Auspzyem956E0+FtDtsx1ZxlOJkQCZ1AFXAY/yuvtFYrOxF+Bw==}
|
||||
engines: {node: ^16.14.0 || >=18.0.0}
|
||||
|
||||
nth-check@2.1.1:
|
||||
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
|
||||
|
||||
nullthrows@1.1.1:
|
||||
resolution: {integrity: sha1-eBgliEOFaulx6uQgitfX6xmkMbE=}
|
||||
|
||||
@@ -2558,6 +2742,14 @@ packages:
|
||||
resolution: {integrity: sha1-yyhoVA4xPWHeWPr741zpAE1VQOY=}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
parent-module@1.0.1:
|
||||
resolution: {integrity: sha1-aR0nCeeMefrjoVZiJFLQB2LKqqI=}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
parse-json@5.2.0:
|
||||
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
parse-png@2.1.0:
|
||||
resolution: {integrity: sha512-Nt/a5SfCLiTnQAjx3fHlqp8hRgTL3z7kTQZzvIMS9uCAepnCyjpdEc6M/sz69WqMBdaDBw9sF1F1UaHROYzGkQ==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -2566,6 +2758,9 @@ packages:
|
||||
resolution: {integrity: sha1-naGee+6NEt/wUT7Vt2lXeTvC6NQ=}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
path-dirname@1.0.2:
|
||||
resolution: {integrity: sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=}
|
||||
|
||||
path-exists@4.0.0:
|
||||
resolution: {integrity: sha1-UTvb4tO5XXdi6METfvoZXGxhtbM=}
|
||||
engines: {node: '>=8'}
|
||||
@@ -2585,6 +2780,10 @@ packages:
|
||||
resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
path-type@4.0.0:
|
||||
resolution: {integrity: sha1-hO0BwKe6OAr+CdkKjBgNzZ0DBDs=}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
@@ -2737,6 +2936,18 @@ packages:
|
||||
react: '*'
|
||||
react-native: '*'
|
||||
|
||||
react-native-svg-transformer@1.5.3:
|
||||
resolution: {integrity: sha512-M4uFg5pUt35OMgjD4rWWbwd6PmxV96W7r/gQTTa+iZA5B+jO6aURhzAZGLHSrg1Kb91cKG0Rildy9q1WJvYstg==}
|
||||
peerDependencies:
|
||||
react-native: '>=0.59.0'
|
||||
react-native-svg: '>=12.0.0'
|
||||
|
||||
react-native-svg@15.12.1:
|
||||
resolution: {integrity: sha512-vCuZJDf8a5aNC2dlMovEv4Z0jjEUET53lm/iILFnFewa15b4atjVxU6Wirm6O9y6dEsdjDZVD7Q3QM4T1wlI8g==}
|
||||
peerDependencies:
|
||||
react: '*'
|
||||
react-native: '*'
|
||||
|
||||
react-native-web@0.21.2:
|
||||
resolution: {integrity: sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==}
|
||||
peerDependencies:
|
||||
@@ -2837,6 +3048,10 @@ packages:
|
||||
resolution: {integrity: sha512-nYzyjnFcPNGR3lx9lwPPPnuQxv6JWEZd2Ci0u9opN7N5zUEPIhY/GbL3vMGOr2UXwEg9WwSyV9X9Y/kLFgPsOg==}
|
||||
engines: {node: '>= 4.0.0'}
|
||||
|
||||
resolve-from@4.0.0:
|
||||
resolution: {integrity: sha1-SrzYUq0y3Xuqv+m0DgCjbbXzkuY=}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
resolve-from@5.0.0:
|
||||
resolution: {integrity: sha1-w1IlhD3493bfIcV1V7wIfp39/Gk=}
|
||||
engines: {node: '>=8'}
|
||||
@@ -2968,6 +3183,9 @@ packages:
|
||||
resolution: {integrity: sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
snake-case@3.0.4:
|
||||
resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==}
|
||||
|
||||
source-map-js@1.2.1:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -3064,6 +3282,14 @@ packages:
|
||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
svg-parser@2.0.4:
|
||||
resolution: {integrity: sha1-/cLinhOVFzYUC3bLEiyO5mMOtrU=}
|
||||
|
||||
svgo@3.3.2:
|
||||
resolution: {integrity: sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
hasBin: true
|
||||
|
||||
tar@7.5.7:
|
||||
resolution: {integrity: sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -4773,6 +4999,87 @@ snapshots:
|
||||
dependencies:
|
||||
'@sinonjs/commons': 3.0.1
|
||||
|
||||
'@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.28.6)':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.6
|
||||
|
||||
'@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.28.6)':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.6
|
||||
|
||||
'@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.28.6)':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.6
|
||||
|
||||
'@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.28.6)':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.6
|
||||
|
||||
'@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.28.6)':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.6
|
||||
|
||||
'@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.28.6)':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.6
|
||||
|
||||
'@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.28.6)':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.6
|
||||
|
||||
'@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.28.6)':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.6
|
||||
|
||||
'@svgr/babel-preset@8.1.0(@babel/core@7.28.6)':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.6
|
||||
'@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.28.6)
|
||||
'@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.28.6)
|
||||
'@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.28.6)
|
||||
'@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.28.6)
|
||||
'@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.28.6)
|
||||
'@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.28.6)
|
||||
'@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.28.6)
|
||||
'@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.28.6)
|
||||
|
||||
'@svgr/core@8.1.0(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.6
|
||||
'@svgr/babel-preset': 8.1.0(@babel/core@7.28.6)
|
||||
camelcase: 6.3.0
|
||||
cosmiconfig: 8.3.6(typescript@5.9.3)
|
||||
snake-case: 3.0.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
'@svgr/hast-util-to-babel-ast@8.0.0':
|
||||
dependencies:
|
||||
'@babel/types': 7.28.6
|
||||
entities: 4.5.0
|
||||
|
||||
'@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.6
|
||||
'@svgr/babel-preset': 8.1.0(@babel/core@7.28.6)
|
||||
'@svgr/core': 8.1.0(typescript@5.9.3)
|
||||
'@svgr/hast-util-to-babel-ast': 8.0.0
|
||||
svg-parser: 2.0.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@svgr/plugin-svgo@8.1.0(@svgr/core@8.1.0(typescript@5.9.3))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@svgr/core': 8.1.0(typescript@5.9.3)
|
||||
cosmiconfig: 8.3.6(typescript@5.9.3)
|
||||
deepmerge: 4.3.1
|
||||
svgo: 3.3.2
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
'@trysound/sax@0.2.0': {}
|
||||
|
||||
'@types/babel__core@7.20.5':
|
||||
dependencies:
|
||||
'@babel/parser': 7.28.6
|
||||
@@ -5053,6 +5360,8 @@ snapshots:
|
||||
|
||||
big-integer@1.6.52: {}
|
||||
|
||||
boolbase@1.0.0: {}
|
||||
|
||||
bplist-creator@0.1.0:
|
||||
dependencies:
|
||||
stream-buffers: 2.2.0
|
||||
@@ -5116,6 +5425,8 @@ snapshots:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
get-intrinsic: 1.3.1
|
||||
|
||||
callsites@3.1.0: {}
|
||||
|
||||
camelcase@5.3.1: {}
|
||||
|
||||
camelcase@6.3.0: {}
|
||||
@@ -5238,6 +5549,15 @@ snapshots:
|
||||
dependencies:
|
||||
browserslist: 4.28.1
|
||||
|
||||
cosmiconfig@8.3.6(typescript@5.9.3):
|
||||
dependencies:
|
||||
import-fresh: 3.3.1
|
||||
js-yaml: 4.1.1
|
||||
parse-json: 5.2.0
|
||||
path-type: 4.0.0
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
|
||||
cross-fetch@3.2.0:
|
||||
dependencies:
|
||||
node-fetch: 2.7.0
|
||||
@@ -5256,6 +5576,35 @@ snapshots:
|
||||
dependencies:
|
||||
hyphenate-style-name: 1.1.0
|
||||
|
||||
css-select@5.2.2:
|
||||
dependencies:
|
||||
boolbase: 1.0.0
|
||||
css-what: 6.2.2
|
||||
domhandler: 5.0.3
|
||||
domutils: 3.2.2
|
||||
nth-check: 2.1.1
|
||||
|
||||
css-tree@1.1.3:
|
||||
dependencies:
|
||||
mdn-data: 2.0.14
|
||||
source-map: 0.6.1
|
||||
|
||||
css-tree@2.2.1:
|
||||
dependencies:
|
||||
mdn-data: 2.0.28
|
||||
source-map-js: 1.2.1
|
||||
|
||||
css-tree@2.3.1:
|
||||
dependencies:
|
||||
mdn-data: 2.0.30
|
||||
source-map-js: 1.2.1
|
||||
|
||||
css-what@6.2.2: {}
|
||||
|
||||
csso@5.0.5:
|
||||
dependencies:
|
||||
css-tree: 2.2.1
|
||||
|
||||
csstype@3.2.3: {}
|
||||
|
||||
debug@2.6.9:
|
||||
@@ -5302,6 +5651,29 @@ snapshots:
|
||||
|
||||
detect-node-es@1.1.0: {}
|
||||
|
||||
dom-serializer@2.0.0:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 5.0.3
|
||||
entities: 4.5.0
|
||||
|
||||
domelementtype@2.3.0: {}
|
||||
|
||||
domhandler@5.0.3:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
|
||||
domutils@3.2.2:
|
||||
dependencies:
|
||||
dom-serializer: 2.0.0
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 5.0.3
|
||||
|
||||
dot-case@3.0.4:
|
||||
dependencies:
|
||||
no-case: 3.0.4
|
||||
tslib: 2.8.1
|
||||
|
||||
dotenv-expand@11.0.7:
|
||||
dependencies:
|
||||
dotenv: 16.4.7
|
||||
@@ -5324,8 +5696,14 @@ snapshots:
|
||||
|
||||
encodeurl@2.0.0: {}
|
||||
|
||||
entities@4.5.0: {}
|
||||
|
||||
env-editor@0.4.2: {}
|
||||
|
||||
error-ex@1.3.4:
|
||||
dependencies:
|
||||
is-arrayish: 0.2.1
|
||||
|
||||
error-stack-parser@2.1.4:
|
||||
dependencies:
|
||||
stackframe: 1.3.4
|
||||
@@ -5396,6 +5774,12 @@ snapshots:
|
||||
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)
|
||||
react: 19.1.0
|
||||
|
||||
expo-linear-gradient@15.0.8(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):
|
||||
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)
|
||||
react: 19.1.0
|
||||
react-native: 0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0)
|
||||
|
||||
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):
|
||||
dependencies:
|
||||
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))
|
||||
@@ -5738,6 +6122,11 @@ snapshots:
|
||||
dependencies:
|
||||
queue: 6.0.2
|
||||
|
||||
import-fresh@3.3.1:
|
||||
dependencies:
|
||||
parent-module: 1.0.1
|
||||
resolve-from: 4.0.0
|
||||
|
||||
imurmurhash@0.1.4: {}
|
||||
|
||||
inflight@1.0.6:
|
||||
@@ -5762,6 +6151,8 @@ snapshots:
|
||||
call-bound: 1.0.4
|
||||
has-tostringtag: 1.0.2
|
||||
|
||||
is-arrayish@0.2.1: {}
|
||||
|
||||
is-arrayish@0.3.4: {}
|
||||
|
||||
is-callable@1.2.7: {}
|
||||
@@ -5909,6 +6300,8 @@ snapshots:
|
||||
|
||||
jsesc@3.1.0: {}
|
||||
|
||||
json-parse-even-better-errors@2.3.1: {}
|
||||
|
||||
json5@2.2.3: {}
|
||||
|
||||
kleur@3.0.3: {}
|
||||
@@ -5995,6 +6388,10 @@ snapshots:
|
||||
dependencies:
|
||||
js-tokens: 4.0.0
|
||||
|
||||
lower-case@2.0.2:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
lru-cache@10.4.3: {}
|
||||
|
||||
lru-cache@11.2.5: {}
|
||||
@@ -6011,6 +6408,12 @@ snapshots:
|
||||
|
||||
math-intrinsics@1.1.0: {}
|
||||
|
||||
mdn-data@2.0.14: {}
|
||||
|
||||
mdn-data@2.0.28: {}
|
||||
|
||||
mdn-data@2.0.30: {}
|
||||
|
||||
memoize-one@5.2.1: {}
|
||||
|
||||
memoize-one@6.0.0: {}
|
||||
@@ -6253,6 +6656,11 @@ snapshots:
|
||||
|
||||
nested-error-stacks@2.0.1: {}
|
||||
|
||||
no-case@3.0.4:
|
||||
dependencies:
|
||||
lower-case: 2.0.2
|
||||
tslib: 2.8.1
|
||||
|
||||
node-fetch@2.7.0:
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
@@ -6272,6 +6680,10 @@ snapshots:
|
||||
semver: 7.7.3
|
||||
validate-npm-package-name: 5.0.1
|
||||
|
||||
nth-check@2.1.1:
|
||||
dependencies:
|
||||
boolbase: 1.0.0
|
||||
|
||||
nullthrows@1.1.1: {}
|
||||
|
||||
ob1@0.83.3:
|
||||
@@ -6352,12 +6764,25 @@ snapshots:
|
||||
|
||||
p-try@2.2.0: {}
|
||||
|
||||
parent-module@1.0.1:
|
||||
dependencies:
|
||||
callsites: 3.1.0
|
||||
|
||||
parse-json@5.2.0:
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.28.6
|
||||
error-ex: 1.3.4
|
||||
json-parse-even-better-errors: 2.3.1
|
||||
lines-and-columns: 1.2.4
|
||||
|
||||
parse-png@2.1.0:
|
||||
dependencies:
|
||||
pngjs: 3.4.0
|
||||
|
||||
parseurl@1.3.3: {}
|
||||
|
||||
path-dirname@1.0.2: {}
|
||||
|
||||
path-exists@4.0.0: {}
|
||||
|
||||
path-is-absolute@1.0.1: {}
|
||||
@@ -6371,6 +6796,8 @@ snapshots:
|
||||
lru-cache: 11.2.5
|
||||
minipass: 7.1.2
|
||||
|
||||
path-type@4.0.0: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
picomatch@2.3.1: {}
|
||||
@@ -6510,6 +6937,26 @@ snapshots:
|
||||
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)
|
||||
warn-once: 0.1.1
|
||||
|
||||
react-native-svg-transformer@1.5.3(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))(react-native@0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0))(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@svgr/core': 8.1.0(typescript@5.9.3)
|
||||
'@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3))
|
||||
'@svgr/plugin-svgo': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3))(typescript@5.9.3)
|
||||
path-dirname: 1.0.2
|
||||
react-native: 0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0)
|
||||
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)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
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):
|
||||
dependencies:
|
||||
css-select: 5.2.2
|
||||
css-tree: 1.1.3
|
||||
react: 19.1.0
|
||||
react-native: 0.81.5(@babel/core@7.28.6)(@types/react@19.1.17)(react@19.1.0)
|
||||
warn-once: 0.1.1
|
||||
|
||||
react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.6
|
||||
@@ -6661,6 +7108,8 @@ snapshots:
|
||||
rc: 1.2.8
|
||||
resolve: 1.7.1
|
||||
|
||||
resolve-from@4.0.0: {}
|
||||
|
||||
resolve-from@5.0.0: {}
|
||||
|
||||
resolve-global@1.0.0:
|
||||
@@ -6786,6 +7235,11 @@ snapshots:
|
||||
|
||||
slugify@1.6.6: {}
|
||||
|
||||
snake-case@3.0.4:
|
||||
dependencies:
|
||||
dot-case: 3.0.4
|
||||
tslib: 2.8.1
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
source-map-support@0.5.21:
|
||||
@@ -6868,6 +7322,18 @@ snapshots:
|
||||
|
||||
supports-preserve-symlinks-flag@1.0.0: {}
|
||||
|
||||
svg-parser@2.0.4: {}
|
||||
|
||||
svgo@3.3.2:
|
||||
dependencies:
|
||||
'@trysound/sax': 0.2.0
|
||||
commander: 7.2.0
|
||||
css-select: 5.2.2
|
||||
css-tree: 2.3.1
|
||||
css-what: 6.2.2
|
||||
csso: 5.0.5
|
||||
picocolors: 1.1.1
|
||||
|
||||
tar@7.5.7:
|
||||
dependencies:
|
||||
'@isaacs/fs-minipass': 4.0.1
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
{
|
||||
"common": {
|
||||
"ok": "OK",
|
||||
"cancel": "Cancel"
|
||||
"cancel": "Cancel",
|
||||
"error": "Error",
|
||||
"openLinkError": "Cannot open link",
|
||||
"back": "Back"
|
||||
},
|
||||
"onboarding": {
|
||||
"title": "Welcome",
|
||||
@@ -33,7 +36,39 @@
|
||||
"like": "Like",
|
||||
"dislike": "Dislike",
|
||||
"favorites": "Favorites",
|
||||
"settings": "Settings"
|
||||
"settings": "Settings",
|
||||
"theme": "Theme",
|
||||
"profile": "Me"
|
||||
},
|
||||
"theme": {
|
||||
"title": "Theme",
|
||||
"scenery": "Scenery",
|
||||
"color": "Color"
|
||||
},
|
||||
"profile": {
|
||||
"title": "Me",
|
||||
"favorites": "My Likes",
|
||||
"widget": "Widget",
|
||||
"dailyReminder": "Daily Reminder",
|
||||
"privacy": "Privacy Policy",
|
||||
"terms": "Terms of Use",
|
||||
"language": "Language",
|
||||
"todoTitle": "Notice",
|
||||
"todoDesc": "This feature is a placeholder for this iteration."
|
||||
},
|
||||
"dailyReminder": {
|
||||
"title": "Daily Reminder",
|
||||
"timesUnit": "times",
|
||||
"pushLabel": "Push Reminder",
|
||||
"ok": "Ok",
|
||||
"minus": "Decrease",
|
||||
"plus": "Increase"
|
||||
},
|
||||
"widget": {
|
||||
"lockScreen": "Lock Screen Widget",
|
||||
"homeScreen": "Home Screen Widget",
|
||||
"previewDate": "Thu, Jan 29",
|
||||
"previewQuote": "I’m proud of who I am, even while becoming who I want to be."
|
||||
},
|
||||
"favorites": {
|
||||
"title": "Favorites",
|
||||
@@ -45,6 +80,12 @@
|
||||
"version": "Version",
|
||||
"widgetTitle": "iOS Widget",
|
||||
"widgetDesc": "Put gentle reminders on your home screen: long-press → tap “+” → search “Mindfulness” → add a size you like."
|
||||
},
|
||||
"consent": {
|
||||
"title": "You Are Perfect.",
|
||||
"subtitle": "Everything Will Be Better.",
|
||||
"agree": "Agree & Continue",
|
||||
"privacy": "Privacy Policy",
|
||||
"terms": "Terms of Use"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"common": {
|
||||
"ok": "Aceptar",
|
||||
"cancel": "Cancelar"
|
||||
"cancel": "Cancelar",
|
||||
"back": "Volver"
|
||||
},
|
||||
"onboarding": {
|
||||
"title": "Bienvenida",
|
||||
@@ -33,7 +34,39 @@
|
||||
"like": "Me gusta",
|
||||
"dislike": "No me gusta",
|
||||
"favorites": "Favoritos",
|
||||
"settings": "Ajustes"
|
||||
"settings": "Ajustes",
|
||||
"theme": "Tema",
|
||||
"profile": "Yo"
|
||||
},
|
||||
"theme": {
|
||||
"title": "Tema",
|
||||
"scenery": "Paisaje",
|
||||
"color": "Color"
|
||||
},
|
||||
"profile": {
|
||||
"title": "Yo",
|
||||
"favorites": "Mis Me gusta",
|
||||
"widget": "Widget",
|
||||
"dailyReminder": "Recordatorio diario",
|
||||
"privacy": "Política de Privacidad",
|
||||
"terms": "Términos de Uso",
|
||||
"language": "Idioma",
|
||||
"todoTitle": "Aviso",
|
||||
"todoDesc": "Esta función es un marcador de posición en esta versión."
|
||||
},
|
||||
"dailyReminder": {
|
||||
"title": "Recordatorio diario",
|
||||
"timesUnit": "veces",
|
||||
"pushLabel": "Recordatorio Push",
|
||||
"ok": "Ok",
|
||||
"minus": "Disminuir",
|
||||
"plus": "Aumentar"
|
||||
},
|
||||
"widget": {
|
||||
"lockScreen": "Widget de pantalla de bloqueo",
|
||||
"homeScreen": "Widget de pantalla de inicio",
|
||||
"previewDate": "Jue, 29 ene",
|
||||
"previewQuote": "Estoy orgullosa de quien soy, incluso mientras me convierto en quien quiero ser."
|
||||
},
|
||||
"favorites": {
|
||||
"title": "Favoritos",
|
||||
@@ -45,6 +78,11 @@
|
||||
"version": "Versión",
|
||||
"widgetTitle": "Widget de iOS",
|
||||
"widgetDesc": "Pon recordatorios en tu pantalla: mantén pulsado → “+” → busca “Mindfulness” → añade el tamaño."
|
||||
},
|
||||
"consent": {
|
||||
"agree": "Aceptar y Continuar",
|
||||
"privacy": "Política de Privacidad",
|
||||
"terms": "Términos de Uso"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"common": {
|
||||
"ok": "OK",
|
||||
"cancel": "Cancelar"
|
||||
"cancel": "Cancelar",
|
||||
"back": "Voltar"
|
||||
},
|
||||
"onboarding": {
|
||||
"title": "Bem-vinda",
|
||||
@@ -33,7 +34,39 @@
|
||||
"like": "Curtir",
|
||||
"dislike": "Não curtir",
|
||||
"favorites": "Favoritos",
|
||||
"settings": "Configurações"
|
||||
"settings": "Configurações",
|
||||
"theme": "Tema",
|
||||
"profile": "Eu"
|
||||
},
|
||||
"theme": {
|
||||
"title": "Tema",
|
||||
"scenery": "Paisagem",
|
||||
"color": "Cor"
|
||||
},
|
||||
"profile": {
|
||||
"title": "Eu",
|
||||
"favorites": "Minhas curtidas",
|
||||
"widget": "Widget",
|
||||
"dailyReminder": "Lembrete diário",
|
||||
"privacy": "Política de Privacidade",
|
||||
"terms": "Termos de Uso",
|
||||
"language": "Idioma",
|
||||
"todoTitle": "Aviso",
|
||||
"todoDesc": "Este recurso é um placeholder nesta versão."
|
||||
},
|
||||
"dailyReminder": {
|
||||
"title": "Lembrete diário",
|
||||
"timesUnit": "vezes",
|
||||
"pushLabel": "Lembrete Push",
|
||||
"ok": "Ok",
|
||||
"minus": "Diminuir",
|
||||
"plus": "Aumentar"
|
||||
},
|
||||
"widget": {
|
||||
"lockScreen": "Widget da tela bloqueada",
|
||||
"homeScreen": "Widget da tela inicial",
|
||||
"previewDate": "Qui, 29 jan",
|
||||
"previewQuote": "Tenho orgulho de quem eu sou, mesmo enquanto me torno quem eu quero ser."
|
||||
},
|
||||
"favorites": {
|
||||
"title": "Favoritos",
|
||||
@@ -45,6 +78,11 @@
|
||||
"version": "Versão",
|
||||
"widgetTitle": "Widget do iOS",
|
||||
"widgetDesc": "Coloque lembretes na tela inicial: pressione e segure → “+” → procure “Mindfulness” → adicione o tamanho."
|
||||
},
|
||||
"consent": {
|
||||
"agree": "Concordar e Continuar",
|
||||
"privacy": "Política de Privacidade",
|
||||
"terms": "Termos de Uso"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
{
|
||||
"common": {
|
||||
"ok": "确定",
|
||||
"cancel": "取消"
|
||||
"cancel": "取消",
|
||||
"error": "错误",
|
||||
"openLinkError": "无法打开链接",
|
||||
"back": "返回"
|
||||
},
|
||||
"onboarding": {
|
||||
"title": "欢迎",
|
||||
@@ -33,7 +36,39 @@
|
||||
"like": "点赞",
|
||||
"dislike": "讨厌",
|
||||
"favorites": "收藏",
|
||||
"settings": "设置"
|
||||
"settings": "设置",
|
||||
"theme": "主题",
|
||||
"profile": "我的"
|
||||
},
|
||||
"theme": {
|
||||
"title": "主题",
|
||||
"scenery": "风景",
|
||||
"color": "颜色"
|
||||
},
|
||||
"profile": {
|
||||
"title": "我的",
|
||||
"favorites": "我的喜欢",
|
||||
"widget": "小组件",
|
||||
"dailyReminder": "每日提醒",
|
||||
"privacy": "隐私政策",
|
||||
"terms": "使用条款",
|
||||
"language": "语言",
|
||||
"todoTitle": "提示",
|
||||
"todoDesc": "该功能本期先占位,后续迭代补齐。"
|
||||
},
|
||||
"dailyReminder": {
|
||||
"title": "每日提醒",
|
||||
"timesUnit": "次",
|
||||
"pushLabel": "推送提醒",
|
||||
"ok": "确定",
|
||||
"minus": "减少次数",
|
||||
"plus": "增加次数"
|
||||
},
|
||||
"widget": {
|
||||
"lockScreen": "锁屏小组件",
|
||||
"homeScreen": "桌面小组件",
|
||||
"previewDate": "1月29日周四 · 已至腊月十一",
|
||||
"previewQuote": "我也对现在的自己感到满意,即使我仍在努力成为想成为的人。"
|
||||
},
|
||||
"favorites": {
|
||||
"title": "收藏夹",
|
||||
@@ -45,6 +80,12 @@
|
||||
"version": "版本",
|
||||
"widgetTitle": "iOS 小组件",
|
||||
"widgetDesc": "把温柔提醒放到桌面上:长按主屏幕 → 点“+” → 搜索“正念” → 添加你喜欢的尺寸。"
|
||||
},
|
||||
"consent": {
|
||||
"title": "你本就完美。",
|
||||
"subtitle": "一切都会变好。",
|
||||
"agree": "同意并继续",
|
||||
"privacy": "隐私协议",
|
||||
"terms": "用户使用协议"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"common": {
|
||||
"ok": "確定",
|
||||
"cancel": "取消"
|
||||
"cancel": "取消",
|
||||
"back": "返回"
|
||||
},
|
||||
"onboarding": {
|
||||
"title": "歡迎",
|
||||
@@ -33,7 +34,39 @@
|
||||
"like": "喜歡",
|
||||
"dislike": "不喜歡",
|
||||
"favorites": "收藏",
|
||||
"settings": "設定"
|
||||
"settings": "設定",
|
||||
"theme": "主題",
|
||||
"profile": "我的"
|
||||
},
|
||||
"theme": {
|
||||
"title": "主題",
|
||||
"scenery": "風景",
|
||||
"color": "顏色"
|
||||
},
|
||||
"profile": {
|
||||
"title": "我的",
|
||||
"favorites": "我的喜歡",
|
||||
"widget": "小工具",
|
||||
"dailyReminder": "每日提醒",
|
||||
"privacy": "隱私政策",
|
||||
"terms": "使用條款",
|
||||
"language": "語言",
|
||||
"todoTitle": "提示",
|
||||
"todoDesc": "此功能本期先占位,後續迭代補齊。"
|
||||
},
|
||||
"dailyReminder": {
|
||||
"title": "每日提醒",
|
||||
"timesUnit": "次",
|
||||
"pushLabel": "推送提醒",
|
||||
"ok": "確定",
|
||||
"minus": "減少次數",
|
||||
"plus": "增加次數"
|
||||
},
|
||||
"widget": {
|
||||
"lockScreen": "鎖屏小工具",
|
||||
"homeScreen": "桌面小工具",
|
||||
"previewDate": "1月29日週四 · 已至臘月十一",
|
||||
"previewQuote": "我也對現在的自己感到滿意,即使我仍在努力成為想成為的人。"
|
||||
},
|
||||
"favorites": {
|
||||
"title": "收藏夾",
|
||||
@@ -45,6 +78,11 @@
|
||||
"version": "版本",
|
||||
"widgetTitle": "iOS 小工具",
|
||||
"widgetDesc": "把溫柔提醒放到桌面上:長按主畫面 → 點「+」 → 搜尋「正念」 → 添加你喜歡的尺寸。"
|
||||
},
|
||||
"consent": {
|
||||
"agree": "同意並繼續",
|
||||
"privacy": "隱私協議",
|
||||
"terms": "用戶使用協議"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,10 +7,24 @@ const KEY_ONBOARDING_COMPLETED = 'onboarding.completed';
|
||||
const KEY_PUSH_PROMPT_STATE = 'push.promptState';
|
||||
const KEY_CONTENT_REACTIONS = 'content.reactions';
|
||||
const KEY_FAVORITES_ITEMS = 'favorites.items';
|
||||
const KEY_CONSENT_ACCEPTED = 'consent.accepted';
|
||||
const KEY_USER_PROFILE = 'user.profile';
|
||||
const KEY_UI_THEME_MODE = 'ui.theme.mode';
|
||||
const KEY_DAILY_REMINDER_SETTINGS = 'dailyReminder.settings';
|
||||
|
||||
export type PushPromptState = 'enabled' | 'skipped' | 'unknown';
|
||||
export type Reaction = 'like' | 'dislike';
|
||||
export type ReactionsMap = Record<string, Reaction>;
|
||||
export type ThemeMode = 'scenery' | 'color';
|
||||
export type UserProfile = {
|
||||
name?: string;
|
||||
intents?: string[];
|
||||
};
|
||||
export type DailyReminderSettings = {
|
||||
timesPerDay: number;
|
||||
pushEnabled: boolean;
|
||||
};
|
||||
|
||||
|
||||
async function getJson<T>(key: string, fallback: T): Promise<T> {
|
||||
const raw = await AsyncStorage.getItem(key);
|
||||
@@ -65,3 +79,50 @@ export async function addFavorite(contentId: string): Promise<void> {
|
||||
await setJson(KEY_FAVORITES_ITEMS, [...list, contentId]);
|
||||
}
|
||||
|
||||
export async function getConsentAccepted(): Promise<boolean> {
|
||||
const raw = await AsyncStorage.getItem(KEY_CONSENT_ACCEPTED);
|
||||
return raw === 'true';
|
||||
}
|
||||
|
||||
export async function setConsentAccepted(accepted: boolean): Promise<void> {
|
||||
await AsyncStorage.setItem(KEY_CONSENT_ACCEPTED, accepted ? 'true' : 'false');
|
||||
}
|
||||
|
||||
export async function getThemeMode(): Promise<ThemeMode> {
|
||||
const raw = await AsyncStorage.getItem(KEY_UI_THEME_MODE);
|
||||
if (raw === 'scenery' || raw === 'color') return raw;
|
||||
return 'scenery';
|
||||
}
|
||||
|
||||
export async function setThemeMode(mode: ThemeMode): Promise<void> {
|
||||
await AsyncStorage.setItem(KEY_UI_THEME_MODE, mode);
|
||||
}
|
||||
|
||||
export async function getUserProfile(): Promise<UserProfile> {
|
||||
return await getJson<UserProfile>(KEY_USER_PROFILE, {});
|
||||
}
|
||||
|
||||
export async function setUserProfile(profile: UserProfile): Promise<void> {
|
||||
const current = await getUserProfile();
|
||||
await setJson(KEY_USER_PROFILE, { ...current, ...profile });
|
||||
}
|
||||
|
||||
export async function getDailyReminderSettings(): Promise<DailyReminderSettings> {
|
||||
const s = await getJson<DailyReminderSettings>(KEY_DAILY_REMINDER_SETTINGS, {
|
||||
timesPerDay: 3,
|
||||
pushEnabled: false,
|
||||
});
|
||||
const timesPerDay = Number.isFinite(s.timesPerDay) ? s.timesPerDay : 3;
|
||||
return {
|
||||
timesPerDay: Math.min(10, Math.max(1, Math.round(timesPerDay))),
|
||||
pushEnabled: Boolean(s.pushEnabled),
|
||||
};
|
||||
}
|
||||
|
||||
export async function setDailyReminderSettings(settings: DailyReminderSettings): Promise<void> {
|
||||
const timesPerDay = Math.min(10, Math.max(1, Math.round(settings.timesPerDay)));
|
||||
await setJson(KEY_DAILY_REMINDER_SETTINGS, {
|
||||
timesPerDay,
|
||||
pushEnabled: Boolean(settings.pushEnabled),
|
||||
});
|
||||
}
|
||||
|
||||
13
client/svg.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
declare module '*.svg' {
|
||||
import * as React from 'react';
|
||||
import { SvgProps } from 'react-native-svg';
|
||||
|
||||
const content: React.FC<SvgProps>;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '*.png' {
|
||||
const content: number;
|
||||
export default content;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/*.d.ts",
|
||||
".expo/types/**/*.ts",
|
||||
"expo-env.d.ts"
|
||||
]
|
||||
|
||||