feat(i18n): 添加国际化支持和中英文切换功能
- 实现完整的中英文国际化系统,支持动态语言切换 - 新增健康数据权限说明页面,提供HealthKit数据使用说明 - 为服药记录添加庆祝动画效果,提升用户体验 - 优化药品添加页面的阴影效果和视觉层次 - 更新个人页面以支持多语言显示和语言选择模态框
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { DateSelector } from '@/components/DateSelector';
|
import { DateSelector } from '@/components/DateSelector';
|
||||||
|
import CelebrationAnimation, { CelebrationAnimationRef } from '@/components/CelebrationAnimation';
|
||||||
import { MedicationCard } from '@/components/medication/MedicationCard';
|
import { MedicationCard } from '@/components/medication/MedicationCard';
|
||||||
import { ThemedText } from '@/components/ThemedText';
|
import { ThemedText } from '@/components/ThemedText';
|
||||||
import { IconSymbol } from '@/components/ui/IconSymbol';
|
import { IconSymbol } from '@/components/ui/IconSymbol';
|
||||||
@@ -15,7 +16,7 @@ import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
|
|||||||
import { Image } from 'expo-image';
|
import { Image } from 'expo-image';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { router } from 'expo-router';
|
import { router } from 'expo-router';
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
ScrollView,
|
ScrollView,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
@@ -39,6 +40,9 @@ export default function MedicationsScreen() {
|
|||||||
const [selectedDate, setSelectedDate] = useState<Dayjs>(dayjs());
|
const [selectedDate, setSelectedDate] = useState<Dayjs>(dayjs());
|
||||||
const [selectedDateIndex, setSelectedDateIndex] = useState<number>(selectedDate.date() - 1);
|
const [selectedDateIndex, setSelectedDateIndex] = useState<number>(selectedDate.date() - 1);
|
||||||
const [activeFilter, setActiveFilter] = useState<MedicationFilter>('all');
|
const [activeFilter, setActiveFilter] = useState<MedicationFilter>('all');
|
||||||
|
const celebrationRef = useRef<CelebrationAnimationRef>(null);
|
||||||
|
const celebrationTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
const [isCelebrationVisible, setIsCelebrationVisible] = useState(false);
|
||||||
|
|
||||||
// 从 Redux 获取数据
|
// 从 Redux 获取数据
|
||||||
const selectedKey = selectedDate.format('YYYY-MM-DD');
|
const selectedKey = selectedDate.format('YYYY-MM-DD');
|
||||||
@@ -59,12 +63,36 @@ export default function MedicationsScreen() {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleMedicationTakenCelebration = useCallback(() => {
|
||||||
|
if (celebrationTimerRef.current) {
|
||||||
|
clearTimeout(celebrationTimerRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsCelebrationVisible(true);
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
celebrationRef.current?.play();
|
||||||
|
});
|
||||||
|
|
||||||
|
celebrationTimerRef.current = setTimeout(() => {
|
||||||
|
setIsCelebrationVisible(false);
|
||||||
|
}, 2400);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 加载药物和记录数据
|
// 加载药物和记录数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(fetchMedications());
|
dispatch(fetchMedications());
|
||||||
dispatch(fetchMedicationRecords({ date: selectedKey }));
|
dispatch(fetchMedicationRecords({ date: selectedKey }));
|
||||||
}, [dispatch, selectedKey]);
|
}, [dispatch, selectedKey]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (celebrationTimerRef.current) {
|
||||||
|
clearTimeout(celebrationTimerRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 页面聚焦时刷新数据,确保从添加页面返回时能看到最新数据
|
// 页面聚焦时刷新数据,确保从添加页面返回时能看到最新数据
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
@@ -126,6 +154,9 @@ export default function MedicationsScreen() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
|
{isCelebrationVisible ? (
|
||||||
|
<CelebrationAnimation ref={celebrationRef} visible={isCelebrationVisible} />
|
||||||
|
) : null}
|
||||||
{/* 背景渐变 */}
|
{/* 背景渐变 */}
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={['#f5e5fbff', '#edf4f4ff', '#ffffff']}
|
colors={['#f5e5fbff', '#edf4f4ff', '#ffffff']}
|
||||||
@@ -278,6 +309,7 @@ export default function MedicationsScreen() {
|
|||||||
colors={colors}
|
colors={colors}
|
||||||
selectedDate={selectedDate}
|
selectedDate={selectedDate}
|
||||||
onOpenDetails={() => handleOpenMedicationDetails(item.medicationId)}
|
onOpenDetails={() => handleOpenMedicationDetails(item.medicationId)}
|
||||||
|
onCelebrate={handleMedicationTakenCelebration}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { getTabBarBottomPadding } from '@/constants/TabBar';
|
|||||||
import { useMembershipModal } from '@/contexts/MembershipModalContext';
|
import { useMembershipModal } from '@/contexts/MembershipModalContext';
|
||||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||||
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||||
import { useNotifications } from '@/hooks/useNotifications';
|
|
||||||
import { selectActiveMembershipPlanName } from '@/store/membershipSlice';
|
import { selectActiveMembershipPlanName } from '@/store/membershipSlice';
|
||||||
import { DEFAULT_MEMBER_NAME, fetchActivityHistory, fetchMyProfile } from '@/store/userSlice';
|
import { DEFAULT_MEMBER_NAME, fetchActivityHistory, fetchMyProfile } from '@/store/userSlice';
|
||||||
import { getItem, setItem } from '@/utils/kvStore';
|
import { getItem, setItem } from '@/utils/kvStore';
|
||||||
@@ -16,27 +15,82 @@ import dayjs from 'dayjs';
|
|||||||
import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
|
import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
|
||||||
import { Image } from 'expo-image';
|
import { Image } from 'expo-image';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
|
import { useRouter } from 'expo-router';
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { Linking, ScrollView, StatusBar, StyleSheet, Switch, Text, TouchableOpacity, View } from 'react-native';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Linking, Modal, ScrollView, StatusBar, StyleSheet, Switch, Text, TouchableOpacity, TouchableWithoutFeedback, View } from 'react-native';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
|
import { AppLanguage, changeAppLanguage, getNormalizedLanguage } from '@/i18n';
|
||||||
|
|
||||||
const DEFAULT_AVATAR_URL = 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/seal-avatar/2.jpeg';
|
const DEFAULT_AVATAR_URL = 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/seal-avatar/2.jpeg';
|
||||||
|
|
||||||
|
type MenuItem = {
|
||||||
|
icon: React.ComponentProps<typeof Ionicons>['name'];
|
||||||
|
title: string;
|
||||||
|
onPress?: () => void;
|
||||||
|
type?: 'switch';
|
||||||
|
switchValue?: boolean;
|
||||||
|
onSwitchChange?: (value: boolean) => void;
|
||||||
|
isDanger?: boolean;
|
||||||
|
rightText?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MenuSectionConfig = {
|
||||||
|
title: string;
|
||||||
|
items: MenuItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type LanguageOption = {
|
||||||
|
code: AppLanguage;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
export default function PersonalScreen() {
|
export default function PersonalScreen() {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { confirmLogout, confirmDeleteAccount, isLoggedIn, pushIfAuthedElseLogin, ensureLoggedIn } = useAuthGuard();
|
const { confirmLogout, confirmDeleteAccount, isLoggedIn, pushIfAuthedElseLogin, ensureLoggedIn } = useAuthGuard();
|
||||||
const { openMembershipModal } = useMembershipModal();
|
const { openMembershipModal } = useMembershipModal();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
const { t, i18n } = useTranslation();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const isLgAvaliable = isLiquidGlassAvailable()
|
const isLgAvaliable = isLiquidGlassAvailable();
|
||||||
|
const [languageModalVisible, setLanguageModalVisible] = useState(false);
|
||||||
|
const [isSwitchingLanguage, setIsSwitchingLanguage] = useState(false);
|
||||||
|
|
||||||
// 推送通知相关
|
const languageOptions = useMemo<LanguageOption[]>(() => ([
|
||||||
const {
|
{
|
||||||
requestPermission,
|
code: 'zh' as AppLanguage,
|
||||||
sendNotification,
|
label: t('personal.language.options.zh.label'),
|
||||||
} = useNotifications();
|
description: t('personal.language.options.zh.description'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'en' as AppLanguage,
|
||||||
|
label: t('personal.language.options.en.label'),
|
||||||
|
description: t('personal.language.options.en.description'),
|
||||||
|
},
|
||||||
|
]), [t]);
|
||||||
|
|
||||||
// 移除 notificationEnabled 状态,因为现在在通知设置页面中管理
|
const activeLanguageCode = getNormalizedLanguage(i18n.language);
|
||||||
|
const activeLanguageLabel = languageOptions.find((option) => option.code === activeLanguageCode)?.label ?? '';
|
||||||
|
|
||||||
|
const handleLanguageSelect = useCallback(async (language: AppLanguage) => {
|
||||||
|
setLanguageModalVisible(false);
|
||||||
|
if (language === activeLanguageCode || isSwitchingLanguage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
setIsSwitchingLanguage(true);
|
||||||
|
await changeAppLanguage(language);
|
||||||
|
} catch (error) {
|
||||||
|
log.warn('语言切换失败', error);
|
||||||
|
} finally {
|
||||||
|
setIsSwitchingLanguage(false);
|
||||||
|
}
|
||||||
|
}, [activeLanguageCode, isSwitchingLanguage]);
|
||||||
|
|
||||||
|
// 推送通知设置仅在独立页面管理
|
||||||
|
|
||||||
// 开发者模式相关状态
|
// 开发者模式相关状态
|
||||||
const [showDeveloperSection, setShowDeveloperSection] = useState(false);
|
const [showDeveloperSection, setShowDeveloperSection] = useState(false);
|
||||||
@@ -111,11 +165,13 @@ export default function PersonalScreen() {
|
|||||||
const birthDate = new Date(userProfile.birthDate);
|
const birthDate = new Date(userProfile.birthDate);
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const age = today.getFullYear() - birthDate.getFullYear();
|
const age = today.getFullYear() - birthDate.getFullYear();
|
||||||
return `${age}岁`;
|
return `${age}${t('personal.stats.ageSuffix')}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 显示名称
|
// 显示名称
|
||||||
const displayName = (userProfile.name?.trim()) ? userProfile.name : DEFAULT_MEMBER_NAME;
|
const displayName = (userProfile.name?.trim()) ? userProfile.name : DEFAULT_MEMBER_NAME;
|
||||||
|
const profileActionLabel = isLoggedIn ? t('personal.edit') : t('personal.login');
|
||||||
|
const aiUsageValue = userProfile.isVip ? t('personal.aiUsageUnlimited') : userProfile.freeUsageCount ?? 0;
|
||||||
|
|
||||||
// 初始化时只加载开发者模式状态
|
// 初始化时只加载开发者模式状态
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -172,13 +228,15 @@ export default function PersonalScreen() {
|
|||||||
<Text style={styles.userName}>{displayName}</Text>
|
<Text style={styles.userName}>{displayName}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
{userProfile.memberNumber && (
|
{userProfile.memberNumber && (
|
||||||
<Text style={styles.userMemberNumber}>会员编号: {userProfile.memberNumber}</Text>
|
<Text style={styles.userMemberNumber}>
|
||||||
|
{t('personal.memberNumber', { number: userProfile.memberNumber })}
|
||||||
|
</Text>
|
||||||
)}
|
)}
|
||||||
{userProfile.freeUsageCount !== undefined && (
|
{userProfile.freeUsageCount !== undefined && (
|
||||||
<View style={styles.aiUsageContainer}>
|
<View style={styles.aiUsageContainer}>
|
||||||
<Ionicons name="sparkles-outline" size={12} color="#9370DB" />
|
<Ionicons name="sparkles-outline" as any size={12} color="#9370DB" />
|
||||||
<Text style={styles.aiUsageText}>
|
<Text style={styles.aiUsageText}>
|
||||||
免费AI次数: {userProfile.isVip ? '无限' : userProfile.freeUsageCount}
|
{t('personal.aiUsage', { value: aiUsageValue })}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
@@ -186,12 +244,12 @@ export default function PersonalScreen() {
|
|||||||
{isLgAvaliable ? (
|
{isLgAvaliable ? (
|
||||||
<TouchableOpacity onPress={() => pushIfAuthedElseLogin('/profile/edit')}>
|
<TouchableOpacity onPress={() => pushIfAuthedElseLogin('/profile/edit')}>
|
||||||
<GlassView style={styles.editButtonGlass}>
|
<GlassView style={styles.editButtonGlass}>
|
||||||
<Text style={styles.editButtonTextGlass}>{isLoggedIn ? '编辑' : '登录'}</Text>
|
<Text style={styles.editButtonTextGlass}>{profileActionLabel}</Text>
|
||||||
</GlassView>
|
</GlassView>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
) : (
|
) : (
|
||||||
<TouchableOpacity style={styles.editButton} onPress={() => pushIfAuthedElseLogin('/profile/edit')}>
|
<TouchableOpacity style={styles.editButton} onPress={() => pushIfAuthedElseLogin('/profile/edit')}>
|
||||||
<Text style={styles.editButtonText}>{isLoggedIn ? '编辑' : '登录'}</Text>
|
<Text style={styles.editButtonText}>{profileActionLabel}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -225,18 +283,18 @@ export default function PersonalScreen() {
|
|||||||
.map((key) => fallbackProfile[key])
|
.map((key) => fallbackProfile[key])
|
||||||
.find((value): value is string => typeof value === 'string' && value.trim().length > 0);
|
.find((value): value is string => typeof value === 'string' && value.trim().length > 0);
|
||||||
|
|
||||||
const rawExpireDate = userProfile.membershipExpiration
|
const rawExpireDate = userProfile.membershipExpiration ?? fallbackExpire;
|
||||||
|
|
||||||
let formattedExpire = '长期有效';
|
let formattedExpire = t('personal.membership.validForever');
|
||||||
if (typeof rawExpireDate === 'string' && rawExpireDate.trim().length > 0) {
|
if (typeof rawExpireDate === 'string' && rawExpireDate.trim().length > 0) {
|
||||||
const parsed = dayjs(rawExpireDate);
|
const parsed = dayjs(rawExpireDate);
|
||||||
formattedExpire = parsed.isValid() ? parsed.format('YYYY年MM月DD日') : rawExpireDate;
|
formattedExpire = parsed.isValid() ? parsed.format(t('personal.membership.dateFormat')) : rawExpireDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
const planName =
|
const planName =
|
||||||
activeMembershipPlanName?.trim() ||
|
activeMembershipPlanName?.trim() ||
|
||||||
userProfile.vipPlanName?.trim() ||
|
userProfile.vipPlanName?.trim() ||
|
||||||
'VIP 会员';
|
t('personal.membership.planFallback');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.sectionContainer}>
|
<View style={styles.sectionContainer}>
|
||||||
@@ -251,21 +309,21 @@ export default function PersonalScreen() {
|
|||||||
<View style={styles.vipCardHeader}>
|
<View style={styles.vipCardHeader}>
|
||||||
<View style={styles.vipCardHeaderLeft}>
|
<View style={styles.vipCardHeaderLeft}>
|
||||||
<View style={styles.vipBadge}>
|
<View style={styles.vipBadge}>
|
||||||
<Ionicons name="sparkles-outline" size={16} color="#FFD361" />
|
<Ionicons name="sparkles-outline" as any size={16} color="#FFD361" />
|
||||||
<Text style={styles.vipBadgeText}>尊享会员</Text>
|
<Text style={styles.vipBadgeText}>{t('personal.membership.badge')}</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.vipCardTitle}>{planName}</Text>
|
<Text style={styles.vipCardTitle}>{planName}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.vipCardIllustration}>
|
<View style={styles.vipCardIllustration}>
|
||||||
<Ionicons name="ribbon" size={32} color="rgba(255,255,255,0.88)" />
|
<Ionicons name="ribbon" as any size={32} color="rgba(255,255,255,0.88)" />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.vipCardFooter}>
|
<View style={styles.vipCardFooter}>
|
||||||
<View style={styles.vipExpiryInfo}>
|
<View style={styles.vipExpiryInfo}>
|
||||||
<Text style={styles.vipExpiryLabel}>会员有效期</Text>
|
<Text style={styles.vipExpiryLabel}>{t('personal.membership.expiryLabel')}</Text>
|
||||||
<View style={styles.vipExpiryRow}>
|
<View style={styles.vipExpiryRow}>
|
||||||
<Ionicons name="time-outline" size={16} color="rgba(255,255,255,0.85)" />
|
<Ionicons name="time-outline" as any size={16} color="rgba(255,255,255,0.85)" />
|
||||||
<Text style={styles.vipExpiryValue}>{formattedExpire}</Text>
|
<Text style={styles.vipExpiryValue}>{formattedExpire}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -276,8 +334,8 @@ export default function PersonalScreen() {
|
|||||||
void handleMembershipPress();
|
void handleMembershipPress();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Ionicons name="swap-horizontal-outline" size={16} color="#2F1767" />
|
<Ionicons name="swap-horizontal-outline" as any size={16} color="#2F1767" />
|
||||||
<Text style={styles.vipChangeButtonText}>更改会员套餐</Text>
|
<Text style={styles.vipChangeButtonText}>{t('personal.membership.changeButton')}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
@@ -294,15 +352,15 @@ export default function PersonalScreen() {
|
|||||||
<View style={styles.statsContainer}>
|
<View style={styles.statsContainer}>
|
||||||
<View style={styles.statItem}>
|
<View style={styles.statItem}>
|
||||||
<Text style={styles.statValue}>{formatHeight()}</Text>
|
<Text style={styles.statValue}>{formatHeight()}</Text>
|
||||||
<Text style={styles.statLabel}>身高</Text>
|
<Text style={styles.statLabel}>{t('personal.stats.height')}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.statItem}>
|
<View style={styles.statItem}>
|
||||||
<Text style={styles.statValue}>{formatWeight()}</Text>
|
<Text style={styles.statValue}>{formatWeight()}</Text>
|
||||||
<Text style={styles.statLabel}>体重</Text>
|
<Text style={styles.statLabel}>{t('personal.stats.weight')}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.statItem}>
|
<View style={styles.statItem}>
|
||||||
<Text style={styles.statValue}>{formatAge()}</Text>
|
<Text style={styles.statValue}>{formatAge()}</Text>
|
||||||
<Text style={styles.statLabel}>年龄</Text>
|
<Text style={styles.statLabel}>{t('personal.stats.age')}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -310,7 +368,7 @@ export default function PersonalScreen() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 菜单项组件
|
// 菜单项组件
|
||||||
const MenuSection = ({ title, items }: { title: string; items: any[] }) => (
|
const MenuSection = ({ title, items }: { title: string; items: MenuItem[] }) => (
|
||||||
<View style={styles.sectionContainer}>
|
<View style={styles.sectionContainer}>
|
||||||
<Text style={styles.sectionTitle}>{title}</Text>
|
<Text style={styles.sectionTitle}>{title}</Text>
|
||||||
<View style={styles.cardContainer}>
|
<View style={styles.cardContainer}>
|
||||||
@@ -324,7 +382,6 @@ export default function PersonalScreen() {
|
|||||||
<View style={styles.menuItemLeft}>
|
<View style={styles.menuItemLeft}>
|
||||||
<View style={[
|
<View style={[
|
||||||
styles.iconContainer,
|
styles.iconContainer,
|
||||||
{ backgroundColor: item.isDanger ? 'rgba(255,68,68,0.1)' : 'rgba(147, 112, 219, 0.1)' }
|
|
||||||
]}>
|
]}>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name={item.icon}
|
name={item.icon}
|
||||||
@@ -343,7 +400,12 @@ export default function PersonalScreen() {
|
|||||||
style={styles.switch}
|
style={styles.switch}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Ionicons name="chevron-forward" size={20} color="#CCCCCC" />
|
<View style={styles.menuRight}>
|
||||||
|
{item.rightText ? (
|
||||||
|
<Text style={styles.menuRightText}>{item.rightText}</Text>
|
||||||
|
) : null}
|
||||||
|
<Ionicons name="chevron-forward" as any size={20} color="#CCCCCC" />
|
||||||
|
</View>
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
))}
|
))}
|
||||||
@@ -352,66 +414,87 @@ export default function PersonalScreen() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 菜单项配置
|
// 菜单项配置
|
||||||
const menuSections = [
|
const menuSections: MenuSectionConfig[] = [
|
||||||
{
|
{
|
||||||
title: '通知',
|
title: t('personal.sections.healthData'),
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
icon: 'notifications-outline' as const,
|
icon: 'medkit-outline' as React.ComponentProps<typeof Ionicons>['name'],
|
||||||
title: '通知设置',
|
title: t('personal.menu.healthDataPermissions'),
|
||||||
|
onPress: () => router.push(ROUTES.HEALTH_DATA_PERMISSIONS),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('personal.sections.notifications'),
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
icon: 'notifications-outline' as React.ComponentProps<typeof Ionicons>['name'],
|
||||||
|
title: t('personal.menu.notificationSettings'),
|
||||||
onPress: () => pushIfAuthedElseLogin(ROUTES.NOTIFICATION_SETTINGS),
|
onPress: () => pushIfAuthedElseLogin(ROUTES.NOTIFICATION_SETTINGS),
|
||||||
}
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('personal.language.title'),
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
icon: 'language-outline' as React.ComponentProps<typeof Ionicons>['name'],
|
||||||
|
title: t('personal.language.menuTitle'),
|
||||||
|
onPress: () => setLanguageModalVisible(true),
|
||||||
|
rightText: activeLanguageLabel,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// 开发者section(需要连续点击三次用户名激活)
|
// 开发者section(需要连续点击三次用户名激活)
|
||||||
...(showDeveloperSection ? [{
|
...(showDeveloperSection ? [{
|
||||||
title: '开发者',
|
title: t('personal.sections.developer'),
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
icon: 'code-slash-outline' as const,
|
icon: 'code-slash-outline' as React.ComponentProps<typeof Ionicons>['name'],
|
||||||
title: '开发者选项',
|
title: t('personal.menu.developerOptions'),
|
||||||
onPress: () => pushIfAuthedElseLogin(ROUTES.DEVELOPER),
|
onPress: () => pushIfAuthedElseLogin(ROUTES.DEVELOPER),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'settings-outline' as const,
|
icon: 'settings-outline' as React.ComponentProps<typeof Ionicons>['name'],
|
||||||
title: '推送通知设置',
|
title: t('personal.menu.pushSettings'),
|
||||||
onPress: () => pushIfAuthedElseLogin('/push-notification-settings'),
|
onPress: () => pushIfAuthedElseLogin(ROUTES.PUSH_NOTIFICATION_SETTINGS),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}] : []),
|
}] : []),
|
||||||
{
|
{
|
||||||
title: '其他',
|
title: t('personal.sections.other'),
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
icon: 'shield-checkmark-outline' as const,
|
icon: 'shield-checkmark-outline' as React.ComponentProps<typeof Ionicons>['name'],
|
||||||
title: '隐私政策',
|
title: t('personal.menu.privacyPolicy'),
|
||||||
onPress: () => Linking.openURL(PRIVACY_POLICY_URL),
|
onPress: () => Linking.openURL(PRIVACY_POLICY_URL),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'chatbubble-ellipses-outline' as const,
|
icon: 'chatbubble-ellipses-outline' as React.ComponentProps<typeof Ionicons>['name'],
|
||||||
title: '意见反馈',
|
title: t('personal.menu.feedback'),
|
||||||
onPress: () => Linking.openURL('mailto:richardwei1995@gmail.com'),
|
onPress: () => Linking.openURL('mailto:richardwei1995@gmail.com'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'document-text-outline' as const,
|
icon: 'document-text-outline' as React.ComponentProps<typeof Ionicons>['name'],
|
||||||
title: '用户协议',
|
title: t('personal.menu.userAgreement'),
|
||||||
onPress: () => Linking.openURL(USER_AGREEMENT_URL),
|
onPress: () => Linking.openURL(USER_AGREEMENT_URL),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// 只有登录用户才显示账号与安全菜单
|
// 只有登录用户才显示账号与安全菜单
|
||||||
...(isLoggedIn ? [{
|
...(isLoggedIn ? [{
|
||||||
title: '账号与安全',
|
title: t('personal.sections.account'),
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
icon: 'log-out-outline' as const,
|
icon: 'log-out-outline' as React.ComponentProps<typeof Ionicons>['name'],
|
||||||
title: '退出登录',
|
title: t('personal.menu.logout'),
|
||||||
onPress: confirmLogout,
|
onPress: confirmLogout,
|
||||||
isDanger: false,
|
isDanger: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'trash-outline' as const,
|
icon: 'trash-outline' as React.ComponentProps<typeof Ionicons>['name'],
|
||||||
title: '注销帐号',
|
title: t('personal.menu.deleteAccount'),
|
||||||
onPress: confirmDeleteAccount,
|
onPress: confirmDeleteAccount,
|
||||||
isDanger: true,
|
isDanger: true,
|
||||||
},
|
},
|
||||||
@@ -419,6 +502,56 @@ export default function PersonalScreen() {
|
|||||||
}] : []),
|
}] : []),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const LanguageSelectorModal = () => (
|
||||||
|
<Modal
|
||||||
|
animationType="fade"
|
||||||
|
transparent
|
||||||
|
visible={languageModalVisible}
|
||||||
|
onRequestClose={() => setLanguageModalVisible(false)}
|
||||||
|
>
|
||||||
|
<View style={styles.languageModalOverlay}>
|
||||||
|
<TouchableWithoutFeedback onPress={() => setLanguageModalVisible(false)}>
|
||||||
|
<View style={styles.languageModalBackdrop} />
|
||||||
|
</TouchableWithoutFeedback>
|
||||||
|
<View style={styles.languageModalContent}>
|
||||||
|
<Text style={styles.languageModalTitle}>{t('personal.language.modalTitle')}</Text>
|
||||||
|
<Text style={styles.languageModalSubtitle}>{t('personal.language.modalSubtitle')}</Text>
|
||||||
|
{languageOptions.map((option) => {
|
||||||
|
const isSelected = option.code === activeLanguageCode;
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={option.code}
|
||||||
|
activeOpacity={0.85}
|
||||||
|
style={[
|
||||||
|
styles.languageOption,
|
||||||
|
isSelected && styles.languageOptionSelected,
|
||||||
|
isSwitchingLanguage && styles.languageOptionDisabled,
|
||||||
|
]}
|
||||||
|
onPress={() => handleLanguageSelect(option.code)}
|
||||||
|
disabled={isSwitchingLanguage}
|
||||||
|
>
|
||||||
|
<View style={styles.languageOptionTextGroup}>
|
||||||
|
<Text style={styles.languageOptionLabel}>{option.label}</Text>
|
||||||
|
<Text style={styles.languageOptionDescription}>{option.description}</Text>
|
||||||
|
</View>
|
||||||
|
{isSelected && (
|
||||||
|
<Ionicons name="checkmark-circle" as any size={20} color="#9370DB" />
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.languageModalClose}
|
||||||
|
onPress={() => setLanguageModalVisible(false)}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
>
|
||||||
|
<Text style={styles.languageModalCloseText}>{t('personal.language.cancel')}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<StatusBar barStyle={'dark-content'} backgroundColor="transparent" translucent />
|
<StatusBar barStyle={'dark-content'} backgroundColor="transparent" translucent />
|
||||||
@@ -455,13 +588,14 @@ export default function PersonalScreen() {
|
|||||||
transition={200}
|
transition={200}
|
||||||
cachePolicy="memory-disk"
|
cachePolicy="memory-disk"
|
||||||
/> */}
|
/> */}
|
||||||
<Text style={styles.fishRecordText}>能量记录</Text>
|
<Text style={styles.fishRecordText}>{t('personal.fishRecord')}</Text>
|
||||||
</View>
|
</View>
|
||||||
<ActivityHeatMap />
|
<ActivityHeatMap />
|
||||||
{menuSections.map((section, index) => (
|
{menuSections.map((section, index) => (
|
||||||
<MenuSection key={index} title={section.title} items={section.items} />
|
<MenuSection key={index} title={section.title} items={section.items} />
|
||||||
))}
|
))}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
<LanguageSelectorModal />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -743,6 +877,15 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
|
menuRight: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
menuRightText: {
|
||||||
|
fontSize: 13,
|
||||||
|
color: '#6C757D',
|
||||||
|
marginRight: 6,
|
||||||
|
},
|
||||||
iconContainer: {
|
iconContainer: {
|
||||||
width: 32,
|
width: 32,
|
||||||
height: 32,
|
height: 32,
|
||||||
@@ -771,4 +914,72 @@ const styles = StyleSheet.create({
|
|||||||
color: '#2C3E50',
|
color: '#2C3E50',
|
||||||
marginLeft: 4,
|
marginLeft: 4,
|
||||||
},
|
},
|
||||||
|
languageModalOverlay: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'stretch',
|
||||||
|
padding: 24,
|
||||||
|
},
|
||||||
|
languageModalBackdrop: {
|
||||||
|
...StyleSheet.absoluteFillObject,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.35)',
|
||||||
|
},
|
||||||
|
languageModalContent: {
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
borderRadius: 18,
|
||||||
|
padding: 20,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 6 },
|
||||||
|
shadowOpacity: 0.15,
|
||||||
|
shadowRadius: 12,
|
||||||
|
elevation: 6,
|
||||||
|
},
|
||||||
|
languageModalTitle: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: '#2C3E50',
|
||||||
|
},
|
||||||
|
languageModalSubtitle: {
|
||||||
|
fontSize: 13,
|
||||||
|
color: '#6C757D',
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
languageOption: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
paddingVertical: 12,
|
||||||
|
paddingHorizontal: 4,
|
||||||
|
},
|
||||||
|
languageOptionSelected: {
|
||||||
|
backgroundColor: 'rgba(147, 112, 219, 0.08)',
|
||||||
|
borderRadius: 12,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
},
|
||||||
|
languageOptionDisabled: {
|
||||||
|
opacity: 0.5,
|
||||||
|
},
|
||||||
|
languageOptionTextGroup: {
|
||||||
|
flex: 1,
|
||||||
|
paddingRight: 12,
|
||||||
|
},
|
||||||
|
languageOptionLabel: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#2C3E50',
|
||||||
|
},
|
||||||
|
languageOptionDescription: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: '#6C757D',
|
||||||
|
marginTop: 4,
|
||||||
|
},
|
||||||
|
languageModalClose: {
|
||||||
|
marginTop: 4,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
languageModalCloseText: {
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: '500',
|
||||||
|
color: '#9370DB',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Stack } from 'expo-router';
|
|||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||||
import 'react-native-reanimated';
|
import 'react-native-reanimated';
|
||||||
|
import '@/i18n';
|
||||||
|
|
||||||
import PrivacyConsentModal from '@/components/PrivacyConsentModal';
|
import PrivacyConsentModal from '@/components/PrivacyConsentModal';
|
||||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||||
@@ -335,6 +336,10 @@ export default function RootLayout() {
|
|||||||
<Stack.Screen name="water-detail" options={{ headerShown: false }} />
|
<Stack.Screen name="water-detail" options={{ headerShown: false }} />
|
||||||
<Stack.Screen name="water-settings" options={{ headerShown: false }} />
|
<Stack.Screen name="water-settings" options={{ headerShown: false }} />
|
||||||
<Stack.Screen name="workout/notification-settings" options={{ headerShown: false }} />
|
<Stack.Screen name="workout/notification-settings" options={{ headerShown: false }} />
|
||||||
|
<Stack.Screen
|
||||||
|
name="health-data-permissions"
|
||||||
|
options={{ headerShown: false }}
|
||||||
|
/>
|
||||||
<Stack.Screen name="+not-found" />
|
<Stack.Screen name="+not-found" />
|
||||||
</Stack>
|
</Stack>
|
||||||
<StatusBar style="dark" />
|
<StatusBar style="dark" />
|
||||||
|
|||||||
248
app/health-data-permissions.tsx
Normal file
248
app/health-data-permissions.tsx
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
import { HeaderBar } from '@/components/ui/HeaderBar';
|
||||||
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Linking, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||||
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
|
type CardConfig = {
|
||||||
|
key: string;
|
||||||
|
icon: React.ComponentProps<typeof Ionicons>['name'];
|
||||||
|
color: string;
|
||||||
|
title: string;
|
||||||
|
items: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function HealthDataPermissionsScreen() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
|
const cards = useMemo<CardConfig[]>(() => ([
|
||||||
|
{
|
||||||
|
key: 'usage',
|
||||||
|
icon: 'pulse-outline',
|
||||||
|
color: '#34D399',
|
||||||
|
title: t('healthPermissions.cards.usage.title'),
|
||||||
|
items: t('healthPermissions.cards.usage.items', { returnObjects: true }) as string[],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'purpose',
|
||||||
|
icon: 'bulb-outline',
|
||||||
|
color: '#FBBF24',
|
||||||
|
title: t('healthPermissions.cards.purpose.title'),
|
||||||
|
items: t('healthPermissions.cards.purpose.items', { returnObjects: true }) as string[],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'control',
|
||||||
|
icon: 'shield-checkmark-outline',
|
||||||
|
color: '#60A5FA',
|
||||||
|
title: t('healthPermissions.cards.control.title'),
|
||||||
|
items: t('healthPermissions.cards.control.items', { returnObjects: true }) as string[],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'privacy',
|
||||||
|
icon: 'lock-closed-outline',
|
||||||
|
color: '#A78BFA',
|
||||||
|
title: t('healthPermissions.cards.privacy.title'),
|
||||||
|
items: t('healthPermissions.cards.privacy.items', { returnObjects: true }) as string[],
|
||||||
|
},
|
||||||
|
]), [t]);
|
||||||
|
|
||||||
|
const calloutItems = useMemo(() => (
|
||||||
|
t('healthPermissions.callout.items', { returnObjects: true }) as string[]
|
||||||
|
), [t]);
|
||||||
|
|
||||||
|
const contactDescription = t('healthPermissions.contact.description');
|
||||||
|
const contactEmail = t('healthPermissions.contact.email');
|
||||||
|
|
||||||
|
const handleContactPress = () => {
|
||||||
|
if (!contactEmail) return;
|
||||||
|
void Linking.openURL(`mailto:${contactEmail}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const contentTopPadding = insets.top + 72;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<HeaderBar
|
||||||
|
title={t('healthPermissions.title')}
|
||||||
|
variant="elevated"
|
||||||
|
transparent={true}
|
||||||
|
/>
|
||||||
|
<ScrollView
|
||||||
|
style={styles.scrollView}
|
||||||
|
contentContainerStyle={{
|
||||||
|
paddingTop: contentTopPadding,
|
||||||
|
paddingBottom: insets.bottom + 32,
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
}}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
|
<View style={styles.heroCard}>
|
||||||
|
<Text style={styles.heroTitle}>{t('healthPermissions.title')}</Text>
|
||||||
|
<Text style={styles.heroSubtitle}>{t('healthPermissions.subtitle')}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{cards.map((card) => (
|
||||||
|
<View key={card.key} style={styles.infoCard}>
|
||||||
|
<View style={styles.cardHeader}>
|
||||||
|
<View style={[styles.cardIcon, { backgroundColor: `${card.color}22` }]}>
|
||||||
|
<Ionicons name={card.icon} size={20} color={card.color} />
|
||||||
|
</View>
|
||||||
|
<Text style={styles.cardTitle}>{card.title}</Text>
|
||||||
|
</View>
|
||||||
|
{card.items.map((item, index) => (
|
||||||
|
<View key={`${card.key}-${index}`} style={styles.cardItemRow}>
|
||||||
|
<View style={styles.bullet} />
|
||||||
|
<Text style={styles.cardItemText}>{item}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<View style={styles.calloutCard}>
|
||||||
|
<View style={styles.cardHeader}>
|
||||||
|
<View style={[styles.cardIcon, { backgroundColor: '#F472B622' }]}>
|
||||||
|
<Ionicons name="alert-circle-outline" size={20} color="#F472B6" />
|
||||||
|
</View>
|
||||||
|
<Text style={styles.cardTitle}>{t('healthPermissions.callout.title')}</Text>
|
||||||
|
</View>
|
||||||
|
{calloutItems.map((item, index) => (
|
||||||
|
<View key={`callout-${index}`} style={styles.cardItemRow}>
|
||||||
|
<View style={styles.bullet} />
|
||||||
|
<Text style={styles.cardItemText}>{item}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.contactCard}>
|
||||||
|
<Text style={styles.contactTitle}>{t('healthPermissions.contact.title')}</Text>
|
||||||
|
<Text style={styles.contactDescription}>{contactDescription}</Text>
|
||||||
|
{contactEmail ? (
|
||||||
|
<TouchableOpacity style={styles.contactButton} onPress={handleContactPress} activeOpacity={0.85}>
|
||||||
|
<Ionicons name="mail-outline" size={18} color="#fff" />
|
||||||
|
<Text style={styles.contactButtonText}>{contactEmail}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#F9FAFB',
|
||||||
|
},
|
||||||
|
scrollView: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
heroCard: {
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderRadius: 20,
|
||||||
|
padding: 20,
|
||||||
|
marginBottom: 16,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOpacity: 0.05,
|
||||||
|
shadowRadius: 10,
|
||||||
|
shadowOffset: { width: 0, height: 8 },
|
||||||
|
elevation: 2,
|
||||||
|
},
|
||||||
|
heroTitle: {
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: '700',
|
||||||
|
color: '#111827',
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
heroSubtitle: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: '#4B5563',
|
||||||
|
lineHeight: 22,
|
||||||
|
},
|
||||||
|
infoCard: {
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderRadius: 18,
|
||||||
|
padding: 18,
|
||||||
|
marginBottom: 14,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#F3F4F6',
|
||||||
|
},
|
||||||
|
cardHeader: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
cardIcon: {
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
borderRadius: 18,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginRight: 10,
|
||||||
|
},
|
||||||
|
cardTitle: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#111827',
|
||||||
|
},
|
||||||
|
cardItemRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
bullet: {
|
||||||
|
width: 6,
|
||||||
|
height: 6,
|
||||||
|
borderRadius: 3,
|
||||||
|
backgroundColor: '#9370DB',
|
||||||
|
marginTop: 8,
|
||||||
|
marginRight: 10,
|
||||||
|
},
|
||||||
|
cardItemText: {
|
||||||
|
flex: 1,
|
||||||
|
fontSize: 14,
|
||||||
|
color: '#374151',
|
||||||
|
lineHeight: 20,
|
||||||
|
},
|
||||||
|
calloutCard: {
|
||||||
|
backgroundColor: '#FEF3F2',
|
||||||
|
borderRadius: 18,
|
||||||
|
padding: 18,
|
||||||
|
marginBottom: 14,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#FECACA',
|
||||||
|
},
|
||||||
|
contactCard: {
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderRadius: 18,
|
||||||
|
padding: 18,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#F3F4F6',
|
||||||
|
},
|
||||||
|
contactTitle: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#111827',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
contactDescription: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: '#4B5563',
|
||||||
|
lineHeight: 20,
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
contactButton: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: '#111827',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 12,
|
||||||
|
borderRadius: 12,
|
||||||
|
},
|
||||||
|
contactButtonText: {
|
||||||
|
marginLeft: 8,
|
||||||
|
color: '#fff',
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -134,6 +134,10 @@ export default function AddMedicationScreen() {
|
|||||||
const glassDisabledTint = useMemo(() => withAlpha(colors.border, theme === 'dark' ? 0.45 : 0.6), [colors.border, theme]);
|
const glassDisabledTint = useMemo(() => withAlpha(colors.border, theme === 'dark' ? 0.45 : 0.6), [colors.border, theme]);
|
||||||
const glassPrimaryBackground = useMemo(() => withAlpha(colors.primary, theme === 'dark' ? 0.35 : 0.7), [colors.primary, theme]);
|
const glassPrimaryBackground = useMemo(() => withAlpha(colors.primary, theme === 'dark' ? 0.35 : 0.7), [colors.primary, theme]);
|
||||||
const glassDisabledBackground = useMemo(() => withAlpha(colors.border, theme === 'dark' ? 0.35 : 0.5), [colors.border, theme]);
|
const glassDisabledBackground = useMemo(() => withAlpha(colors.border, theme === 'dark' ? 0.35 : 0.5), [colors.border, theme]);
|
||||||
|
const cardShadowColor = useMemo(
|
||||||
|
() => (theme === 'dark' ? 'rgba(15, 23, 42, 0.45)' : 'rgba(15, 23, 42, 0.16)'),
|
||||||
|
[theme]
|
||||||
|
);
|
||||||
|
|
||||||
const [photoPreview, setPhotoPreview] = useState<string | null>(null);
|
const [photoPreview, setPhotoPreview] = useState<string | null>(null);
|
||||||
const [photoUrl, setPhotoUrl] = useState<string | null>(null);
|
const [photoUrl, setPhotoUrl] = useState<string | null>(null);
|
||||||
@@ -617,9 +621,10 @@ export default function AddMedicationScreen() {
|
|||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
styles.searchField,
|
styles.searchField,
|
||||||
|
styles.inputShadow,
|
||||||
{
|
{
|
||||||
backgroundColor: colors.surface,
|
backgroundColor: colors.surface,
|
||||||
borderColor: softBorderColor,
|
shadowColor: cardShadowColor,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -687,9 +692,10 @@ export default function AddMedicationScreen() {
|
|||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
styles.dosageField,
|
styles.dosageField,
|
||||||
|
styles.inputShadow,
|
||||||
{
|
{
|
||||||
borderColor: softBorderColor,
|
|
||||||
backgroundColor: colors.surface,
|
backgroundColor: colors.surface,
|
||||||
|
shadowColor: cardShadowColor,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -868,7 +874,16 @@ export default function AddMedicationScreen() {
|
|||||||
<View style={styles.stepSection}>
|
<View style={styles.stepSection}>
|
||||||
<View style={styles.inputGroup}>
|
<View style={styles.inputGroup}>
|
||||||
<ThemedText style={[styles.groupLabel, { color: colors.textSecondary }]}>备注</ThemedText>
|
<ThemedText style={[styles.groupLabel, { color: colors.textSecondary }]}>备注</ThemedText>
|
||||||
<View style={styles.noteInputWrapper}>
|
<View
|
||||||
|
style={[
|
||||||
|
styles.noteInputWrapper,
|
||||||
|
styles.inputShadow,
|
||||||
|
{
|
||||||
|
backgroundColor: colors.surface,
|
||||||
|
shadowColor: dictationActive ? colors.primary : cardShadowColor,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
<TextInput
|
<TextInput
|
||||||
multiline
|
multiline
|
||||||
numberOfLines={4}
|
numberOfLines={4}
|
||||||
@@ -879,7 +894,6 @@ export default function AddMedicationScreen() {
|
|||||||
style={[
|
style={[
|
||||||
styles.noteInput,
|
styles.noteInput,
|
||||||
{
|
{
|
||||||
borderColor: dictationActive ? colors.primary : softBorderColor,
|
|
||||||
backgroundColor: colors.surface,
|
backgroundColor: colors.surface,
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
},
|
},
|
||||||
@@ -1357,10 +1371,17 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
borderWidth: 1,
|
|
||||||
height: 56,
|
height: 56,
|
||||||
gap: 12,
|
gap: 12,
|
||||||
},
|
},
|
||||||
|
inputShadow: {
|
||||||
|
borderWidth: 0,
|
||||||
|
shadowColor: 'rgba(15, 23, 42, 0.16)',
|
||||||
|
shadowOffset: { width: 0, height: 8 },
|
||||||
|
shadowOpacity: 0.12,
|
||||||
|
shadowRadius: 14,
|
||||||
|
elevation: 6,
|
||||||
|
},
|
||||||
searchInput: {
|
searchInput: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
@@ -1451,7 +1472,6 @@ const styles = StyleSheet.create({
|
|||||||
letterSpacing: 0.2,
|
letterSpacing: 0.2,
|
||||||
},
|
},
|
||||||
dosageField: {
|
dosageField: {
|
||||||
borderWidth: 1,
|
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
paddingVertical: 12,
|
paddingVertical: 12,
|
||||||
@@ -1582,7 +1602,6 @@ const styles = StyleSheet.create({
|
|||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
noteInput: {
|
noteInput: {
|
||||||
borderWidth: 1,
|
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
paddingRight: 72,
|
paddingRight: 72,
|
||||||
@@ -1594,6 +1613,7 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
noteInputWrapper: {
|
noteInputWrapper: {
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
|
borderRadius: 24,
|
||||||
},
|
},
|
||||||
noteVoiceButton: {
|
noteVoiceButton: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
|||||||
@@ -14,9 +14,10 @@ export type MedicationCardProps = {
|
|||||||
colors: (typeof import('@/constants/Colors').Colors)[keyof typeof import('@/constants/Colors').Colors];
|
colors: (typeof import('@/constants/Colors').Colors)[keyof typeof import('@/constants/Colors').Colors];
|
||||||
selectedDate: Dayjs;
|
selectedDate: Dayjs;
|
||||||
onOpenDetails?: (medication: MedicationDisplayItem) => void;
|
onOpenDetails?: (medication: MedicationDisplayItem) => void;
|
||||||
|
onCelebrate?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function MedicationCard({ medication, colors, selectedDate, onOpenDetails }: MedicationCardProps) {
|
export function MedicationCard({ medication, colors, selectedDate, onOpenDetails, onCelebrate }: MedicationCardProps) {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [imageError, setImageError] = useState(false);
|
const [imageError, setImageError] = useState(false);
|
||||||
@@ -81,6 +82,7 @@ export function MedicationCard({ medication, colors, selectedDate, onOpenDetails
|
|||||||
recordId: recordId,
|
recordId: recordId,
|
||||||
actualTime: new Date().toISOString(),
|
actualTime: new Date().toISOString(),
|
||||||
})).unwrap();
|
})).unwrap();
|
||||||
|
onCelebrate?.();
|
||||||
|
|
||||||
// 可选:显示成功提示
|
// 可选:显示成功提示
|
||||||
// Alert.alert('服药成功', '已记录本次服药');
|
// Alert.alert('服药成功', '已记录本次服药');
|
||||||
|
|||||||
@@ -61,6 +61,9 @@ export const ROUTES = {
|
|||||||
// 新用户引导
|
// 新用户引导
|
||||||
ONBOARDING: '/onboarding',
|
ONBOARDING: '/onboarding',
|
||||||
|
|
||||||
|
// 健康权限披露
|
||||||
|
HEALTH_DATA_PERMISSIONS: '/health-data-permissions',
|
||||||
|
|
||||||
|
|
||||||
// 目标管理路由 (已移至tab中)
|
// 目标管理路由 (已移至tab中)
|
||||||
// GOAL_MANAGEMENT: '/goal-management',
|
// GOAL_MANAGEMENT: '/goal-management',
|
||||||
@@ -71,6 +74,7 @@ export const ROUTES = {
|
|||||||
|
|
||||||
// 通知设置路由
|
// 通知设置路由
|
||||||
NOTIFICATION_SETTINGS: '/notification-settings',
|
NOTIFICATION_SETTINGS: '/notification-settings',
|
||||||
|
PUSH_NOTIFICATION_SETTINGS: '/push-notification-settings',
|
||||||
|
|
||||||
// 药品相关路由
|
// 药品相关路由
|
||||||
MEDICATION_EDIT_FREQUENCY: '/medications/edit-frequency',
|
MEDICATION_EDIT_FREQUENCY: '/medications/edit-frequency',
|
||||||
|
|||||||
298
i18n/index.ts
Normal file
298
i18n/index.ts
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
import * as Localization from 'expo-localization';
|
||||||
|
import i18n from 'i18next';
|
||||||
|
import { initReactI18next } from 'react-i18next';
|
||||||
|
|
||||||
|
import { getItemSync, setItem } from '@/utils/kvStore';
|
||||||
|
|
||||||
|
export const LANGUAGE_PREFERENCE_KEY = 'app_language_preference';
|
||||||
|
export const SUPPORTED_LANGUAGES = ['zh', 'en'] as const;
|
||||||
|
export type AppLanguage = typeof SUPPORTED_LANGUAGES[number];
|
||||||
|
|
||||||
|
const fallbackLanguage: AppLanguage = 'zh';
|
||||||
|
|
||||||
|
const personalScreenResources = {
|
||||||
|
edit: '编辑',
|
||||||
|
login: '登录',
|
||||||
|
memberNumber: '会员编号: {{number}}',
|
||||||
|
aiUsage: '免费AI次数: {{value}}',
|
||||||
|
aiUsageUnlimited: '无限',
|
||||||
|
fishRecord: '能量记录',
|
||||||
|
stats: {
|
||||||
|
height: '身高',
|
||||||
|
weight: '体重',
|
||||||
|
age: '年龄',
|
||||||
|
ageSuffix: '岁',
|
||||||
|
},
|
||||||
|
membership: {
|
||||||
|
badge: '尊享会员',
|
||||||
|
planFallback: 'VIP 会员',
|
||||||
|
expiryLabel: '会员有效期',
|
||||||
|
changeButton: '更改会员套餐',
|
||||||
|
validForever: '长期有效',
|
||||||
|
dateFormat: 'YYYY年MM月DD日',
|
||||||
|
},
|
||||||
|
sections: {
|
||||||
|
notifications: '通知',
|
||||||
|
developer: '开发者',
|
||||||
|
other: '其他',
|
||||||
|
account: '账号与安全',
|
||||||
|
language: '语言',
|
||||||
|
healthData: '健康数据授权',
|
||||||
|
},
|
||||||
|
menu: {
|
||||||
|
notificationSettings: '通知设置',
|
||||||
|
developerOptions: '开发者选项',
|
||||||
|
pushSettings: '推送通知设置',
|
||||||
|
privacyPolicy: '隐私政策',
|
||||||
|
feedback: '意见反馈',
|
||||||
|
userAgreement: '用户协议',
|
||||||
|
logout: '退出登录',
|
||||||
|
deleteAccount: '注销帐号',
|
||||||
|
healthDataPermissions: '健康数据授权说明',
|
||||||
|
},
|
||||||
|
language: {
|
||||||
|
title: '语言',
|
||||||
|
menuTitle: '界面语言',
|
||||||
|
modalTitle: '选择语言',
|
||||||
|
modalSubtitle: '选择后界面会立即更新',
|
||||||
|
cancel: '取消',
|
||||||
|
options: {
|
||||||
|
zh: {
|
||||||
|
label: '中文',
|
||||||
|
description: '推荐中文用户使用',
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
label: '英文',
|
||||||
|
description: '使用英文界面',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const healthPermissionsResources = {
|
||||||
|
title: '健康数据授权说明',
|
||||||
|
subtitle: '我们通过 Apple Health 的 HealthKit/CareKit 接口同步必要的数据,让训练、恢复和提醒更贴合你的身体状态。',
|
||||||
|
cards: {
|
||||||
|
usage: {
|
||||||
|
title: '我们会读取 / 写入的数据',
|
||||||
|
items: [
|
||||||
|
'运动与活动:步数、活动能量、锻炼记录用于生成训练表现和热力图。',
|
||||||
|
'身体指标:身高、体重、体脂率帮助制定个性化训练与营养建议。',
|
||||||
|
'睡眠与恢复:睡眠时长与阶段用于智能提醒与恢复建议。',
|
||||||
|
'水分摄入:读取与写入饮水记录,保持与「健康」App 一致。',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
purpose: {
|
||||||
|
title: '使用这些数据的目的',
|
||||||
|
items: [
|
||||||
|
'提供个性化训练计划、挑战与恢复建议。',
|
||||||
|
'在统计页展示长期趋势,帮助你理解身体变化。',
|
||||||
|
'减少重复输入,在提醒与挑战中自动同步进度。',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
control: {
|
||||||
|
title: '你的控制权',
|
||||||
|
items: [
|
||||||
|
'授权流程完全由 Apple Health 控制,你可随时在 iOS 设置 > 健康 > 数据访问与设备 中更改权限。',
|
||||||
|
'未授权的数据不会被访问,撤销授权后我们会清理相关缓存。',
|
||||||
|
'核心功能依旧可用,并提供手动输入等替代方案。',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
privacy: {
|
||||||
|
title: '数据存储与隐私',
|
||||||
|
items: [
|
||||||
|
'健康数据仅存储在你的设备上,我们不会上传服务器或共享给第三方。',
|
||||||
|
'只有在需要同步的功能中才会保存聚合后的匿名统计值。',
|
||||||
|
'我们遵循 Apple 的审核要求,任何变更都会提前告知。',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
callout: {
|
||||||
|
title: '未授权会怎样?',
|
||||||
|
items: [
|
||||||
|
'相关模块会提示你授权,并提供手动记录入口。',
|
||||||
|
'拒绝授权不会影响其它与健康数据无关的功能。',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
contact: {
|
||||||
|
title: '需要更多帮助?',
|
||||||
|
description: '如果你对 HealthKit / CareKit 的使用方式有疑问,可通过以下邮箱或在个人中心提交反馈:',
|
||||||
|
email: 'richardwei1995@gmail.com',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const resources = {
|
||||||
|
zh: {
|
||||||
|
translation: {
|
||||||
|
personal: personalScreenResources,
|
||||||
|
healthPermissions: healthPermissionsResources,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
translation: {
|
||||||
|
personal: {
|
||||||
|
edit: 'Edit',
|
||||||
|
login: 'Log in',
|
||||||
|
memberNumber: 'Member ID: {{number}}',
|
||||||
|
aiUsage: 'Free AI credits: {{value}}',
|
||||||
|
aiUsageUnlimited: 'Unlimited',
|
||||||
|
fishRecord: 'Energy log',
|
||||||
|
stats: {
|
||||||
|
height: 'Height',
|
||||||
|
weight: 'Weight',
|
||||||
|
age: 'Age',
|
||||||
|
ageSuffix: ' yrs',
|
||||||
|
},
|
||||||
|
membership: {
|
||||||
|
badge: 'Premium member',
|
||||||
|
planFallback: 'VIP Membership',
|
||||||
|
expiryLabel: 'Valid until',
|
||||||
|
changeButton: 'Change plan',
|
||||||
|
validForever: 'No expiry',
|
||||||
|
dateFormat: 'YYYY-MM-DD',
|
||||||
|
},
|
||||||
|
sections: {
|
||||||
|
notifications: 'Notifications',
|
||||||
|
developer: 'Developer',
|
||||||
|
other: 'Other',
|
||||||
|
account: 'Account & Security',
|
||||||
|
language: 'Language',
|
||||||
|
healthData: 'Health data permissions',
|
||||||
|
},
|
||||||
|
menu: {
|
||||||
|
notificationSettings: 'Notification settings',
|
||||||
|
developerOptions: 'Developer options',
|
||||||
|
pushSettings: 'Push notification settings',
|
||||||
|
privacyPolicy: 'Privacy policy',
|
||||||
|
feedback: 'Feedback',
|
||||||
|
userAgreement: 'User agreement',
|
||||||
|
logout: 'Log out',
|
||||||
|
deleteAccount: 'Delete account',
|
||||||
|
healthDataPermissions: 'Health data disclosure',
|
||||||
|
},
|
||||||
|
language: {
|
||||||
|
title: 'Language',
|
||||||
|
menuTitle: 'Display language',
|
||||||
|
modalTitle: 'Choose language',
|
||||||
|
modalSubtitle: 'Your selection applies immediately',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
options: {
|
||||||
|
zh: {
|
||||||
|
label: 'Chinese',
|
||||||
|
description: 'Use the Chinese interface',
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
label: 'English',
|
||||||
|
description: 'Use the app in English',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
healthPermissions: {
|
||||||
|
title: 'Health data disclosure',
|
||||||
|
subtitle: 'We integrate with Apple Health through HealthKit and CareKit to deliver precise training, recovery, and reminder experiences.',
|
||||||
|
cards: {
|
||||||
|
usage: {
|
||||||
|
title: 'Data we read or write',
|
||||||
|
items: [
|
||||||
|
'Activity: steps, active energy, and workouts fuel performance charts and rings.',
|
||||||
|
'Body metrics: height, weight, and body fat keep plans and nutrition tips personalized.',
|
||||||
|
'Sleep & recovery: duration and stages unlock recovery advice and reminders.',
|
||||||
|
'Hydration: we read and write water intake so Health and the app stay in sync.',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
purpose: {
|
||||||
|
title: 'Why we need it',
|
||||||
|
items: [
|
||||||
|
'Generate adaptive training plans, challenges, and recovery nudges.',
|
||||||
|
'Display long-term trends so you can understand progress at a glance.',
|
||||||
|
'Reduce manual input by syncing reminders and challenge progress automatically.',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
control: {
|
||||||
|
title: 'Your control',
|
||||||
|
items: [
|
||||||
|
'Permissions are granted inside Apple Health; change them anytime under iOS Settings > Health > Data Access & Devices.',
|
||||||
|
'We never access data you do not authorize, and cached values are removed if you revoke access.',
|
||||||
|
'Core functionality keeps working and offers manual input alternatives.',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
privacy: {
|
||||||
|
title: 'Storage & privacy',
|
||||||
|
items: [
|
||||||
|
'Health data stays on your device — we do not upload it or share it with third parties.',
|
||||||
|
'Only aggregated, anonymized stats are synced when absolutely necessary.',
|
||||||
|
'We follow Apple’s review requirements and will notify you before any changes.',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
callout: {
|
||||||
|
title: 'What if I skip authorization?',
|
||||||
|
items: [
|
||||||
|
'The related modules will ask for permission and provide manual logging options.',
|
||||||
|
'Declining does not break other areas of the app that do not rely on Health data.',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
contact: {
|
||||||
|
title: 'Need help?',
|
||||||
|
description: 'Questions about HealthKit or CareKit? Reach out via email or the in-app feedback form:',
|
||||||
|
email: 'richardwei1995@gmail.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isSupportedLanguage = (language?: string | null): language is AppLanguage => {
|
||||||
|
if (!language) return false;
|
||||||
|
return SUPPORTED_LANGUAGES.some((code) => language === code || language.startsWith(`${code}-`));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getNormalizedLanguage = (language?: string | null): AppLanguage => {
|
||||||
|
if (!language) return fallbackLanguage;
|
||||||
|
const normalized = SUPPORTED_LANGUAGES.find((code) => language === code || language.startsWith(`${code}-`));
|
||||||
|
return normalized ?? fallbackLanguage;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStoredLanguage = (): AppLanguage | null => {
|
||||||
|
try {
|
||||||
|
const stored = getItemSync?.(LANGUAGE_PREFERENCE_KEY) as AppLanguage | null | undefined;
|
||||||
|
if (stored && isSupportedLanguage(stored)) {
|
||||||
|
return stored;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// ignore storage errors and fall back to device preference
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDeviceLanguage = (): AppLanguage | null => {
|
||||||
|
try {
|
||||||
|
const locales = Localization.getLocales();
|
||||||
|
const preferred = locales.find((locale) => locale.languageCode && isSupportedLanguage(locale.languageCode));
|
||||||
|
return preferred?.languageCode as AppLanguage | undefined || null;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialLanguage = getStoredLanguage() ?? getDeviceLanguage() ?? fallbackLanguage;
|
||||||
|
|
||||||
|
void i18n.use(initReactI18next).init({
|
||||||
|
compatibilityJSON: 'v4',
|
||||||
|
resources,
|
||||||
|
lng: initialLanguage,
|
||||||
|
fallbackLng: fallbackLanguage,
|
||||||
|
interpolation: {
|
||||||
|
escapeValue: false,
|
||||||
|
},
|
||||||
|
returnNull: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const changeAppLanguage = async (language: AppLanguage) => {
|
||||||
|
const nextLanguage = isSupportedLanguage(language) ? language : fallbackLanguage;
|
||||||
|
await i18n.changeLanguage(nextLanguage);
|
||||||
|
await setItem(LANGUAGE_PREFERENCE_KEY, nextLanguage);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default i18n;
|
||||||
@@ -16,10 +16,10 @@
|
|||||||
79B2CB702E7B954600B51753 /* OutLive-Bridging-Header.h in Sources */ = {isa = PBXBuildFile; fileRef = F11748442D0722820044C1D9 /* OutLive-Bridging-Header.h */; };
|
79B2CB702E7B954600B51753 /* OutLive-Bridging-Header.h in Sources */ = {isa = PBXBuildFile; fileRef = F11748442D0722820044C1D9 /* OutLive-Bridging-Header.h */; };
|
||||||
79B2CB732E7B954F00B51753 /* HealthKitManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 79B2CB712E7B954F00B51753 /* HealthKitManager.m */; };
|
79B2CB732E7B954F00B51753 /* HealthKitManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 79B2CB712E7B954F00B51753 /* HealthKitManager.m */; };
|
||||||
79B2CB742E7B954F00B51753 /* HealthKitManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79B2CB722E7B954F00B51753 /* HealthKitManager.swift */; };
|
79B2CB742E7B954F00B51753 /* HealthKitManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79B2CB722E7B954F00B51753 /* HealthKitManager.swift */; };
|
||||||
B6B9273B2FD4F4A800C6391C /* BackgroundTaskBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B9273A2FD4F4A800C6391C /* BackgroundTaskBridge.swift */; };
|
|
||||||
B6B9273D2FD4F4A800C6391C /* BackgroundTaskBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = B6B9273C2FD4F4A800C6391C /* BackgroundTaskBridge.m */; };
|
|
||||||
91B7BA17B50D328546B5B4B8 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = B7F23062EE59F61E6260DBA8 /* PrivacyInfo.xcprivacy */; };
|
91B7BA17B50D328546B5B4B8 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = B7F23062EE59F61E6260DBA8 /* PrivacyInfo.xcprivacy */; };
|
||||||
AE00ECEC9D078460F642F131 /* libPods-OutLive.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6F6136AA7113B3D210693D88 /* libPods-OutLive.a */; };
|
AE00ECEC9D078460F642F131 /* libPods-OutLive.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6F6136AA7113B3D210693D88 /* libPods-OutLive.a */; };
|
||||||
|
B6B9273B2FD4F4A800C6391C /* BackgroundTaskBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B9273A2FD4F4A800C6391C /* BackgroundTaskBridge.swift */; };
|
||||||
|
B6B9273D2FD4F4A800C6391C /* BackgroundTaskBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = B6B9273C2FD4F4A800C6391C /* BackgroundTaskBridge.m */; };
|
||||||
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
|
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
|
||||||
F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11748412D0307B40044C1D9 /* AppDelegate.swift */; };
|
F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11748412D0307B40044C1D9 /* AppDelegate.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
@@ -37,9 +37,9 @@
|
|||||||
79B2CB712E7B954F00B51753 /* HealthKitManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = HealthKitManager.m; path = OutLive/HealthKitManager.m; sourceTree = "<group>"; };
|
79B2CB712E7B954F00B51753 /* HealthKitManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = HealthKitManager.m; path = OutLive/HealthKitManager.m; sourceTree = "<group>"; };
|
||||||
79B2CB722E7B954F00B51753 /* HealthKitManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = HealthKitManager.swift; path = OutLive/HealthKitManager.swift; sourceTree = "<group>"; };
|
79B2CB722E7B954F00B51753 /* HealthKitManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = HealthKitManager.swift; path = OutLive/HealthKitManager.swift; sourceTree = "<group>"; };
|
||||||
9B6A6CEBED2FC0931F7B7236 /* Pods-OutLive.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OutLive.release.xcconfig"; path = "Target Support Files/Pods-OutLive/Pods-OutLive.release.xcconfig"; sourceTree = "<group>"; };
|
9B6A6CEBED2FC0931F7B7236 /* Pods-OutLive.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OutLive.release.xcconfig"; path = "Target Support Files/Pods-OutLive/Pods-OutLive.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = OutLive/SplashScreen.storyboard; sourceTree = "<group>"; };
|
||||||
B6B9273A2FD4F4A800C6391C /* BackgroundTaskBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BackgroundTaskBridge.swift; path = OutLive/BackgroundTaskBridge.swift; sourceTree = "<group>"; };
|
B6B9273A2FD4F4A800C6391C /* BackgroundTaskBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BackgroundTaskBridge.swift; path = OutLive/BackgroundTaskBridge.swift; sourceTree = "<group>"; };
|
||||||
B6B9273C2FD4F4A800C6391C /* BackgroundTaskBridge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = BackgroundTaskBridge.m; path = OutLive/BackgroundTaskBridge.m; sourceTree = "<group>"; };
|
B6B9273C2FD4F4A800C6391C /* BackgroundTaskBridge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = BackgroundTaskBridge.m; path = OutLive/BackgroundTaskBridge.m; sourceTree = "<group>"; };
|
||||||
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = OutLive/SplashScreen.storyboard; sourceTree = "<group>"; };
|
|
||||||
B7F23062EE59F61E6260DBA8 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = OutLive/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
B7F23062EE59F61E6260DBA8 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = OutLive/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||||
BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = "<group>"; };
|
BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = "<group>"; };
|
||||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
|
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
|
||||||
@@ -277,6 +277,7 @@
|
|||||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXNotifications/ExpoNotifications_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/EXNotifications/ExpoNotifications_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXTaskManager/ExpoTaskManager_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/EXTaskManager/ExpoTaskManager_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoLocalization/ExpoLocalization_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoMediaLibrary/ExpoMediaLibrary_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoMediaLibrary/ExpoMediaLibrary_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoSystemUI/ExpoSystemUI_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoSystemUI/ExpoSystemUI_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/PurchasesHybridCommon/PurchasesHybridCommon.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/PurchasesHybridCommon/PurchasesHybridCommon.bundle",
|
||||||
@@ -299,6 +300,7 @@
|
|||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoNotifications_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoNotifications_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoTaskManager_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoTaskManager_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
|
"${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}/ExpoMediaLibrary_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoMediaLibrary_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoSystemUI_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoSystemUI_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/PurchasesHybridCommon.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/PurchasesHybridCommon.bundle",
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.0.25</string>
|
<string>1.0.24</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ require File.join(File.dirname(`node --print "require.resolve('react-native/pack
|
|||||||
require 'json'
|
require 'json'
|
||||||
podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {}
|
podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {}
|
||||||
|
|
||||||
ENV['RCT_NEW_ARCH_ENABLED'] ||= '0' if podfile_properties['newArchEnabled'] == 'false'
|
new_arch_flag = podfile_properties['newArchEnabled']
|
||||||
|
new_arch_enabled = ['true', true].include?(new_arch_flag)
|
||||||
|
new_arch_disabled = ['false', false].include?(new_arch_flag)
|
||||||
|
|
||||||
|
ENV['RCT_NEW_ARCH_ENABLED'] = '1' if new_arch_enabled
|
||||||
|
ENV['RCT_NEW_ARCH_ENABLED'] ||= '0' if new_arch_disabled
|
||||||
ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] ||= podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR']
|
ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] ||= podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR']
|
||||||
ENV['RCT_USE_RN_DEP'] ||= '1' if podfile_properties['ios.buildReactNativeFromSource'] != 'true' && podfile_properties['newArchEnabled'] != 'false'
|
ENV['RCT_USE_RN_DEP'] ||= '1' if podfile_properties['ios.buildReactNativeFromSource'] != 'true' && podfile_properties['newArchEnabled'] != 'false'
|
||||||
ENV['RCT_USE_PREBUILT_RNCORE'] ||= '1' if podfile_properties['ios.buildReactNativeFromSource'] != 'true' && podfile_properties['newArchEnabled'] != 'false'
|
ENV['RCT_USE_PREBUILT_RNCORE'] ||= '1' if podfile_properties['ios.buildReactNativeFromSource'] != 'true' && podfile_properties['newArchEnabled'] != 'false'
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ PODS:
|
|||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- ExpoLinking (8.0.8):
|
- ExpoLinking (8.0.8):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
|
- ExpoLocalization (17.0.7):
|
||||||
|
- ExpoModulesCore
|
||||||
- ExpoMediaLibrary (18.2.0):
|
- ExpoMediaLibrary (18.2.0):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- React-Core
|
- React-Core
|
||||||
@@ -2325,6 +2327,7 @@ DEPENDENCIES:
|
|||||||
- ExpoKeepAwake (from `../node_modules/expo-keep-awake/ios`)
|
- ExpoKeepAwake (from `../node_modules/expo-keep-awake/ios`)
|
||||||
- ExpoLinearGradient (from `../node_modules/expo-linear-gradient/ios`)
|
- ExpoLinearGradient (from `../node_modules/expo-linear-gradient/ios`)
|
||||||
- ExpoLinking (from `../node_modules/expo-linking/ios`)
|
- ExpoLinking (from `../node_modules/expo-linking/ios`)
|
||||||
|
- ExpoLocalization (from `../node_modules/expo-localization/ios`)
|
||||||
- ExpoMediaLibrary (from `../node_modules/expo-media-library/ios`)
|
- ExpoMediaLibrary (from `../node_modules/expo-media-library/ios`)
|
||||||
- ExpoModulesCore (from `../node_modules/expo-modules-core`)
|
- ExpoModulesCore (from `../node_modules/expo-modules-core`)
|
||||||
- ExpoQuickActions (from `../node_modules/expo-quick-actions/ios`)
|
- ExpoQuickActions (from `../node_modules/expo-quick-actions/ios`)
|
||||||
@@ -2479,6 +2482,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: "../node_modules/expo-linear-gradient/ios"
|
:path: "../node_modules/expo-linear-gradient/ios"
|
||||||
ExpoLinking:
|
ExpoLinking:
|
||||||
:path: "../node_modules/expo-linking/ios"
|
:path: "../node_modules/expo-linking/ios"
|
||||||
|
ExpoLocalization:
|
||||||
|
:path: "../node_modules/expo-localization/ios"
|
||||||
ExpoMediaLibrary:
|
ExpoMediaLibrary:
|
||||||
:path: "../node_modules/expo-media-library/ios"
|
:path: "../node_modules/expo-media-library/ios"
|
||||||
ExpoModulesCore:
|
ExpoModulesCore:
|
||||||
@@ -2694,6 +2699,7 @@ SPEC CHECKSUMS:
|
|||||||
ExpoKeepAwake: 1a2e820692e933c94a565ec3fbbe38ac31658ffe
|
ExpoKeepAwake: 1a2e820692e933c94a565ec3fbbe38ac31658ffe
|
||||||
ExpoLinearGradient: a464898cb95153125e3b81894fd479bcb1c7dd27
|
ExpoLinearGradient: a464898cb95153125e3b81894fd479bcb1c7dd27
|
||||||
ExpoLinking: f051f28e50ea9269ff539317c166adec81d9342d
|
ExpoLinking: f051f28e50ea9269ff539317c166adec81d9342d
|
||||||
|
ExpoLocalization: b852a5d8ec14c5349c1593eca87896b5b3ebfcca
|
||||||
ExpoMediaLibrary: 641a6952299b395159ccd459bd8f5f6764bf55fe
|
ExpoMediaLibrary: 641a6952299b395159ccd459bd8f5f6764bf55fe
|
||||||
ExpoModulesCore: 5f20603cf25698682d7c43c05fbba8c748b189d2
|
ExpoModulesCore: 5f20603cf25698682d7c43c05fbba8c748b189d2
|
||||||
ExpoQuickActions: 31a70aa6a606128de4416a4830e09cfabfe6667f
|
ExpoQuickActions: 31a70aa6a606128de4416a4830e09cfabfe6667f
|
||||||
@@ -2803,6 +2809,6 @@ SPEC CHECKSUMS:
|
|||||||
Yoga: 5934998fbeaef7845dbf698f698518695ab4cd1a
|
Yoga: 5934998fbeaef7845dbf698f698518695ab4cd1a
|
||||||
ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5
|
ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5
|
||||||
|
|
||||||
PODFILE CHECKSUM: 78eca51725b1f0fcd006b70b9a09e3fb4f960d03
|
PODFILE CHECKSUM: eaa675c9798afb03f0e80539fe72dae01c73dd1e
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|||||||
106
package-lock.json
generated
106
package-lock.json
generated
@@ -36,6 +36,7 @@
|
|||||||
"expo-image-picker": "~17.0.8",
|
"expo-image-picker": "~17.0.8",
|
||||||
"expo-linear-gradient": "~15.0.7",
|
"expo-linear-gradient": "~15.0.7",
|
||||||
"expo-linking": "~8.0.8",
|
"expo-linking": "~8.0.8",
|
||||||
|
"expo-localization": "^17.0.7",
|
||||||
"expo-media-library": "^18.2.0",
|
"expo-media-library": "^18.2.0",
|
||||||
"expo-notifications": "~0.32.12",
|
"expo-notifications": "~0.32.12",
|
||||||
"expo-quick-actions": "^6.0.0",
|
"expo-quick-actions": "^6.0.0",
|
||||||
@@ -47,10 +48,12 @@
|
|||||||
"expo-system-ui": "~6.0.8",
|
"expo-system-ui": "~6.0.8",
|
||||||
"expo-task-manager": "~14.0.8",
|
"expo-task-manager": "~14.0.8",
|
||||||
"expo-web-browser": "~15.0.7",
|
"expo-web-browser": "~15.0.7",
|
||||||
|
"i18next": "^25.6.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lottie-react-native": "^7.3.4",
|
"lottie-react-native": "^7.3.4",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
|
"react-i18next": "^16.3.0",
|
||||||
"react-native": "0.81.5",
|
"react-native": "0.81.5",
|
||||||
"react-native-chart-kit": "^6.12.0",
|
"react-native-chart-kit": "^6.12.0",
|
||||||
"react-native-device-info": "^14.0.4",
|
"react-native-device-info": "^14.0.4",
|
||||||
@@ -7502,6 +7505,19 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-localization": {
|
||||||
|
"version": "17.0.7",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/expo-localization/-/expo-localization-17.0.7.tgz",
|
||||||
|
"integrity": "sha512-ACg1B0tJLNa+f8mZfAaNrMyNzrrzHAARVH1sHHvh+LolKdQpgSKX69Uroz1Llv4C71furpwBklVStbNcEwVVVA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"rtl-detect": "^1.0.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*",
|
||||||
|
"react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-media-library": {
|
"node_modules/expo-media-library": {
|
||||||
"version": "18.2.0",
|
"version": "18.2.0",
|
||||||
"resolved": "https://mirrors.tencent.com/npm/expo-media-library/-/expo-media-library-18.2.0.tgz",
|
"resolved": "https://mirrors.tencent.com/npm/expo-media-library/-/expo-media-library-18.2.0.tgz",
|
||||||
@@ -8823,6 +8839,15 @@
|
|||||||
"resolved": "https://mirrors.tencent.com/npm/lru-cache/-/lru-cache-10.4.3.tgz",
|
"resolved": "https://mirrors.tencent.com/npm/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="
|
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/html-parse-stringify": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"void-elements": "3.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/html2canvas": {
|
"node_modules/html2canvas": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://mirrors.tencent.com/npm/html2canvas/-/html2canvas-1.4.1.tgz",
|
"resolved": "https://mirrors.tencent.com/npm/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||||
@@ -8963,6 +8988,37 @@
|
|||||||
"integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==",
|
"integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==",
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/i18next": {
|
||||||
|
"version": "25.6.2",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/i18next/-/i18next-25.6.2.tgz",
|
||||||
|
"integrity": "sha512-0GawNyVUw0yvJoOEBq1VHMAsqdM23XrHkMtl2gKEjviJQSLVXsrPqsoYAxBEugW5AB96I2pZkwRxyl8WZVoWdw==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://locize.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://locize.com/i18next.html"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.27.6"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ieee754": {
|
"node_modules/ieee754": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||||
@@ -12005,6 +12061,33 @@
|
|||||||
"react": ">=17.0.0"
|
"react": ">=17.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-i18next": {
|
||||||
|
"version": "16.3.0",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/react-i18next/-/react-i18next-16.3.0.tgz",
|
||||||
|
"integrity": "sha512-XGYIVU6gCOL4UQsfp87WbbvBc2WvgdkEDI8r4TwACzFg1bXY8pd1d9Cw6u9WJ2soTKHKaF1xQEyWA3/dUvtAGw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.27.6",
|
||||||
|
"html-parse-stringify": "^3.0.1",
|
||||||
|
"use-sync-external-store": "^1.6.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"i18next": ">= 25.6.2",
|
||||||
|
"react": ">= 16.8.0",
|
||||||
|
"typescript": "^5"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-native": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-is": {
|
"node_modules/react-is": {
|
||||||
"version": "19.2.0",
|
"version": "19.2.0",
|
||||||
"resolved": "https://mirrors.tencent.com/npm/react-is/-/react-is-19.2.0.tgz",
|
"resolved": "https://mirrors.tencent.com/npm/react-is/-/react-is-19.2.0.tgz",
|
||||||
@@ -12891,6 +12974,12 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rtl-detect": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/rtl-detect/-/rtl-detect-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-PGMBq03+TTG/p/cRB7HCLKJ1MgDIi07+QU1faSjiYRfmY5UsAttV9Hs08jDAHVwcOwmVLcSJkpwyfXszVjWfIQ==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/run-parallel": {
|
"node_modules/run-parallel": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||||
@@ -14236,7 +14325,7 @@
|
|||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"resolved": "https://mirrors.tencent.com/npm/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://mirrors.tencent.com/npm/typescript/-/typescript-5.9.3.tgz",
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
@@ -14522,9 +14611,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/use-sync-external-store": {
|
"node_modules/use-sync-external-store": {
|
||||||
"version": "1.5.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
|
"resolved": "https://mirrors.tencent.com/npm/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||||
"integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
|
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
@@ -14784,6 +14873,15 @@
|
|||||||
"integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==",
|
"integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/void-elements": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/void-elements/-/void-elements-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/walker": {
|
"node_modules/walker": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
"expo-image-picker": "~17.0.8",
|
"expo-image-picker": "~17.0.8",
|
||||||
"expo-linear-gradient": "~15.0.7",
|
"expo-linear-gradient": "~15.0.7",
|
||||||
"expo-linking": "~8.0.8",
|
"expo-linking": "~8.0.8",
|
||||||
|
"expo-localization": "^17.0.7",
|
||||||
"expo-media-library": "^18.2.0",
|
"expo-media-library": "^18.2.0",
|
||||||
"expo-notifications": "~0.32.12",
|
"expo-notifications": "~0.32.12",
|
||||||
"expo-quick-actions": "^6.0.0",
|
"expo-quick-actions": "^6.0.0",
|
||||||
@@ -48,10 +49,12 @@
|
|||||||
"expo-system-ui": "~6.0.8",
|
"expo-system-ui": "~6.0.8",
|
||||||
"expo-task-manager": "~14.0.8",
|
"expo-task-manager": "~14.0.8",
|
||||||
"expo-web-browser": "~15.0.7",
|
"expo-web-browser": "~15.0.7",
|
||||||
|
"i18next": "^25.6.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lottie-react-native": "^7.3.4",
|
"lottie-react-native": "^7.3.4",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
|
"react-i18next": "^16.3.0",
|
||||||
"react-native": "0.81.5",
|
"react-native": "0.81.5",
|
||||||
"react-native-chart-kit": "^6.12.0",
|
"react-native-chart-kit": "^6.12.0",
|
||||||
"react-native-device-info": "^14.0.4",
|
"react-native-device-info": "^14.0.4",
|
||||||
|
|||||||
Reference in New Issue
Block a user