feat: 添加相机和相册权限请求功能

- 在 AI 体态评估页面中集成相机和相册权限请求逻辑
- 更新 app.json 和 Info.plist,添加相应的权限说明
- 修改布局以支持照片上传功能,用户可上传正面、侧面和背面照片
- 更新 package.json 和 package-lock.json,添加 expo-image-picker 依赖
This commit is contained in:
richarjiang
2025-08-12 17:30:26 +08:00
parent e84ad0857c
commit 2fac3f899c
9 changed files with 705 additions and 365 deletions

View File

@@ -12,7 +12,10 @@
"supportsTablet": true, "supportsTablet": true,
"bundleIdentifier": "com.anonymous.digitalpilates", "bundleIdentifier": "com.anonymous.digitalpilates",
"infoPlist": { "infoPlist": {
"ITSAppUsesNonExemptEncryption": false "ITSAppUsesNonExemptEncryption": false,
"NSCameraUsageDescription": "应用需要使用相机以拍摄您的体态照片用于AI测评。",
"NSPhotoLibraryUsageDescription": "应用需要访问相册以选择您的体态照片用于AI测评。",
"NSPhotoLibraryAddUsageDescription": "应用需要写入相册以保存拍摄的体态照片(可选)。"
} }
}, },
"android": { "android": {

View File

@@ -4,9 +4,13 @@ import React from 'react';
import { Text, TouchableOpacity, View } from 'react-native'; import { Text, TouchableOpacity, View } from 'react-native';
import { IconSymbol } from '@/components/ui/IconSymbol'; import { IconSymbol } from '@/components/ui/IconSymbol';
import { Colors } from '@/constants/Colors';
import { TAB_BAR_BOTTOM_OFFSET, TAB_BAR_HEIGHT } from '@/constants/TabBar'; import { TAB_BAR_BOTTOM_OFFSET, TAB_BAR_HEIGHT } from '@/constants/TabBar';
import { useColorScheme } from '@/hooks/useColorScheme';
export default function TabLayout() { export default function TabLayout() {
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
const colorTokens = Colors[theme];
const pathname = usePathname(); const pathname = usePathname();
return ( return (
@@ -19,9 +23,9 @@ export default function TabLayout() {
return { return {
headerShown: false, headerShown: false,
tabBarActiveTintColor: '#192126', tabBarActiveTintColor: colorTokens.tabIconSelected,
tabBarButton: (props) => { tabBarButton: (props) => {
const { children, onPress } = props; const { onPress } = props;
const handlePress = (event: any) => { const handlePress = (event: any) => {
if (process.env.EXPO_OS === 'ios') { if (process.env.EXPO_OS === 'ios') {
@@ -30,9 +34,28 @@ export default function TabLayout() {
onPress && onPress(event); onPress && onPress(event);
}; };
// 基于 routeName 设置图标与标题,避免 tabBarIcon 的包装导致文字裁剪
const getIconAndTitle = () => {
switch (routeName) {
case 'index':
return { icon: 'house.fill', title: '首页' } as const;
case 'explore':
return { icon: 'paperplane.fill', title: '探索' } as const;
case 'personal':
return { icon: 'person.fill', title: '个人' } as const;
default:
return { icon: 'circle', title: '' } as const;
}
};
const { icon, title } = getIconAndTitle();
const activeContentColor = colorTokens.onPrimary;
const inactiveContentColor = colorTokens.tabIconDefault;
return ( return (
<TouchableOpacity <TouchableOpacity
onPress={handlePress} onPress={handlePress}
accessibilityRole="button"
style={{ style={{
flex: 1, flex: 1,
alignItems: 'center', alignItems: 'center',
@@ -41,12 +64,32 @@ export default function TabLayout() {
marginHorizontal: 6, marginHorizontal: 6,
marginVertical: 10, marginVertical: 10,
borderRadius: 25, borderRadius: 25,
backgroundColor: isSelected ? '#BBF246' : 'transparent', backgroundColor: isSelected ? colorTokens.tabBarActiveBackground : 'transparent',
paddingHorizontal: isSelected ? 16 : 8, paddingHorizontal: isSelected ? 16 : 10,
paddingVertical: 8, paddingVertical: 8,
}} }}
> >
{children} <View style={{ flexDirection: 'row', alignItems: 'center' }}>
<IconSymbol
size={22}
name={icon as any}
color={isSelected ? activeContentColor : inactiveContentColor}
/>
{isSelected && !!title && (
<Text
style={{
color: activeContentColor,
fontSize: 12,
fontWeight: '600',
marginLeft: 6,
}}
// 选中态下不限制行数,避免大屏布局下被裁剪成省略号
numberOfLines={0 as any}
>
{title}
</Text>
)}
</View>
</TouchableOpacity> </TouchableOpacity>
); );
}, },
@@ -55,7 +98,7 @@ export default function TabLayout() {
bottom: TAB_BAR_BOTTOM_OFFSET, bottom: TAB_BAR_BOTTOM_OFFSET,
height: TAB_BAR_HEIGHT, height: TAB_BAR_HEIGHT,
borderRadius: 34, borderRadius: 34,
backgroundColor: '#192126', backgroundColor: colorTokens.tabBarBackground,
shadowColor: '#000', shadowColor: '#000',
shadowOffset: { width: 0, height: 2 }, shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2, shadowOpacity: 0.2,

View File

@@ -1,9 +1,13 @@
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { BlurView } from 'expo-blur';
import * as ImagePicker from 'expo-image-picker';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import React from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { import {
Alert,
Image, Image,
ImageBackground, Linking,
Platform,
ScrollView, ScrollView,
StyleSheet, StyleSheet,
Text, Text,
@@ -14,49 +18,157 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Colors } from '@/constants/Colors'; import { Colors } from '@/constants/Colors';
type Exercise = { type PoseView = 'front' | 'side' | 'back';
id: string;
title: string; type UploadState = {
duration: string; front?: string | null;
imageUri: string; side?: string | null;
back?: string | null;
}; };
const EXERCISES: Exercise[] = [ type Sample = { uri: string; correct: boolean };
{
id: '1', const SAMPLES: Record<PoseView, Sample[]> = {
title: 'Jumping Jacks', front: [
duration: '00:30', { uri: 'https://images.unsplash.com/photo-1594737625785-c6683fc87c73?w=400&q=80&auto=format', correct: true },
imageUri: { uri: 'https://images.unsplash.com/photo-1544716278-ca5e3f4abd8c?w=400&q=80&auto=format', correct: false },
'https://images.unsplash.com/photo-1546483875-ad9014c88eba?q=80&w=400&auto=format&fit=crop', { uri: 'https://images.unsplash.com/photo-1571019614242-c5c5dee9f50b?w=400&q=80&auto=format', correct: false },
}, ],
{ side: [
id: '2', { uri: 'https://images.unsplash.com/photo-1554463529-e27854014799?w=400&q=80&auto=format', correct: true },
title: 'Squats', { uri: 'https://images.unsplash.com/photo-1596357395104-5bcae0b1a5eb?w=400&q=80&auto=format', correct: false },
duration: '00:45', { uri: 'https://images.unsplash.com/photo-1526506118085-60ce8714f8c5?w=400&q=80&auto=format', correct: false },
imageUri: ],
'https://images.unsplash.com/photo-1583454110551-21f2fa2f36f0?q=80&w=400&auto=format&fit=crop', back: [
}, { uri: 'https://images.unsplash.com/photo-1517836357463-d25dfeac3438?w=400&q=80&auto=format', correct: true },
{ { uri: 'https://images.unsplash.com/photo-1571721797421-f4c9f2b13107?w=400&q=80&auto=format', correct: false },
id: '3', { uri: 'https://images.unsplash.com/photo-1518611012118-696072aa579a?w=400&q=80&auto=format', correct: false },
title: 'Backward Lunge', ],
duration: '00:40', };
imageUri:
'https://images.unsplash.com/photo-1597074866923-5c3bfa3b6c46?q=80&w=400&auto=format&fit=crop',
},
{
id: '4',
title: 'High Knees',
duration: '00:30',
imageUri:
'https://images.unsplash.com/photo-1596357395104-5bcae0b1a5eb?q=80&w=400&auto=format&fit=crop',
},
];
export default function AIPostureAssessmentScreen() { export default function AIPostureAssessmentScreen() {
const router = useRouter(); const router = useRouter();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const theme = Colors.dark;
const theme = Colors.dark; // 该页面采用深色视觉 const [uploadState, setUploadState] = useState<UploadState>({});
const canStart = useMemo(
() => Boolean(uploadState.front && uploadState.side && uploadState.back),
[uploadState]
);
const [cameraPerm, setCameraPerm] = useState<ImagePicker.PermissionStatus | null>(null);
const [libraryPerm, setLibraryPerm] = useState<ImagePicker.PermissionStatus | null>(null);
const [libraryAccess, setLibraryAccess] = useState<'all' | 'limited' | 'none' | null>(null);
const [cameraCanAsk, setCameraCanAsk] = useState<boolean | null>(null);
const [libraryCanAsk, setLibraryCanAsk] = useState<boolean | null>(null);
useEffect(() => {
(async () => {
const cam = await ImagePicker.getCameraPermissionsAsync();
const lib = await ImagePicker.getMediaLibraryPermissionsAsync();
setCameraPerm(cam.status);
setLibraryPerm(lib.status);
setLibraryAccess(
(lib as any).accessPrivileges ?? (lib.status === 'granted' ? 'all' : 'none')
);
setCameraCanAsk(cam.canAskAgain);
setLibraryCanAsk(lib.canAskAgain);
})();
}, []);
async function requestAllPermissions() {
try {
const cam = await ImagePicker.requestCameraPermissionsAsync();
const lib = await ImagePicker.requestMediaLibraryPermissionsAsync();
setCameraPerm(cam.status);
setLibraryPerm(lib.status);
setLibraryAccess(
(lib as any).accessPrivileges ?? (lib.status === 'granted' ? 'all' : 'none')
);
setCameraCanAsk(cam.canAskAgain);
setLibraryCanAsk(lib.canAskAgain);
const libGranted = lib.status === 'granted' || (lib as any).accessPrivileges === 'limited';
if (cam.status !== 'granted' || !libGranted) {
Alert.alert(
'权限未完全授予',
'请在系统设置中授予相机与相册权限以完成上传',
[
{ text: '取消', style: 'cancel' },
{ text: '去设置', onPress: () => Linking.openSettings() },
]
);
}
} catch { }
}
async function requestPermissionAndPick(source: 'camera' | 'library', key: PoseView) {
try {
if (source === 'camera') {
const resp = await ImagePicker.requestCameraPermissionsAsync();
setCameraPerm(resp.status);
setCameraCanAsk(resp.canAskAgain);
if (resp.status !== 'granted') {
Alert.alert(
'权限不足',
'需要相机权限以拍摄照片',
resp.canAskAgain
? [{ text: '好的' }]
: [
{ text: '取消', style: 'cancel' },
{ text: '去设置', onPress: () => Linking.openSettings() },
]
);
return;
}
const result = await ImagePicker.launchCameraAsync({
allowsEditing: true,
quality: 0.8,
aspect: [3, 4],
});
if (!result.canceled) {
setUploadState((s) => ({ ...s, [key]: result.assets[0]?.uri ?? null }));
}
} else {
const resp = await ImagePicker.requestMediaLibraryPermissionsAsync();
setLibraryPerm(resp.status);
setLibraryAccess(
(resp as any).accessPrivileges ?? (resp.status === 'granted' ? 'all' : 'none')
);
setLibraryCanAsk(resp.canAskAgain);
const libGranted = resp.status === 'granted' || (resp as any).accessPrivileges === 'limited';
if (!libGranted) {
Alert.alert(
'权限不足',
'需要相册权限以选择照片',
resp.canAskAgain
? [{ text: '好的' }]
: [
{ text: '取消', style: 'cancel' },
{ text: '去设置', onPress: () => Linking.openSettings() },
]
);
return;
}
const result = await ImagePicker.launchImageLibraryAsync({
allowsEditing: true,
quality: 0.8,
aspect: [3, 4],
});
if (!result.canceled) {
setUploadState((s) => ({ ...s, [key]: result.assets[0]?.uri ?? null }));
}
}
} catch (e) {
Alert.alert('发生错误', '选择图片失败,请重试');
}
}
function handleStart() {
if (!canStart) return;
// TODO: 调用后端或进入分析页面
Alert.alert('开始测评', '已收集三视角照片准备开始AI体态分析');
}
return ( return (
<View style={[styles.screen, { backgroundColor: theme.background }]}> <View style={[styles.screen, { backgroundColor: theme.background }]}>
@@ -69,7 +181,7 @@ export default function AIPostureAssessmentScreen() {
> >
<Ionicons name="chevron-back" size={24} color="#ECEDEE" /> <Ionicons name="chevron-back" size={24} color="#ECEDEE" />
</TouchableOpacity> </TouchableOpacity>
<Text style={styles.headerTitle}>AI体态评</Text> <Text style={styles.headerTitle}>AI体态</Text>
<View style={{ width: 32 }} /> <View style={{ width: 32 }} />
</View> </View>
@@ -77,104 +189,142 @@ export default function AIPostureAssessmentScreen() {
contentContainerStyle={{ paddingBottom: insets.bottom + 120 }} contentContainerStyle={{ paddingBottom: insets.bottom + 120 }}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
> >
{/* Hero */} {/* Permissions Banner (iOS 优先提示) */}
<View style={styles.heroContainer}> {Platform.OS === 'ios' && (
<ImageBackground (cameraPerm !== 'granted' || !(libraryPerm === 'granted' || libraryAccess === 'limited')) && (
source={{ <BlurView intensity={18} tint="dark" style={styles.permBanner}>
uri: <Text style={styles.permTitle}></Text>
'https://images.unsplash.com/photo-1594737625785-c6683fc87c73?q=80&w=1200&auto=format&fit=crop', <Text style={styles.permDesc}>
}} AI体态测评
style={styles.heroImage} </Text>
imageStyle={{ borderRadius: 28 }} <View style={styles.permActions}>
> {((cameraCanAsk ?? true) || (libraryCanAsk ?? true)) ? (
<View style={styles.heroOverlay} /> <TouchableOpacity style={styles.permPrimary} onPress={requestAllPermissions}>
</ImageBackground> <Text style={styles.permPrimaryText}></Text>
</TouchableOpacity>
) : (
<TouchableOpacity style={styles.permPrimary} onPress={() => Linking.openSettings()}>
<Text style={styles.permPrimaryText}></Text>
</TouchableOpacity>
)}
<TouchableOpacity style={styles.permSecondary} onPress={() => requestPermissionAndPick('library', 'front')}>
<Text style={styles.permSecondaryText}></Text>
</TouchableOpacity>
</View>
</BlurView>
)
)}
{/* Floating stats */} {/* Intro */}
<View style={styles.statsFloating}> <View style={styles.introBox}>
<StatCard <Text style={styles.title}>姿</Text>
icon={<Ionicons name="time-outline" size={18} color="#192126" />}
label="Time"
value="20 min"
/>
<View style={styles.divider} />
<StatCard
icon={<Ionicons name="flame-outline" size={18} color="#192126" />}
label="Burn"
value="95 kcal"
/>
</View>
</View>
{/* Title & description */}
<View style={styles.contentSection}>
<Text style={styles.title}>Lower Body Training</Text>
<Text style={styles.description}> <Text style={styles.description}>
The lower abdomen and hips are the most difficult areas of the body to reduce when we are on 线
a diet. Even so, in this area, especially the legs as a whole, you can reduce weight even if
you don't use tools.
</Text> </Text>
</View> </View>
{/* Rounds header */} {/* Upload sections */}
<View style={styles.roundsHeader}> <UploadTile
<Text style={styles.roundsTitle}>Rounds</Text> label="正面"
<Text style={styles.roundsCount}>1/8</Text> value={uploadState.front}
</View> onPickCamera={() => requestPermissionAndPick('camera', 'front')}
onPickLibrary={() => requestPermissionAndPick('library', 'front')}
samples={SAMPLES.front}
/>
{/* Exercise list */} <UploadTile
<View style={{ gap: 14, marginHorizontal: 20 }}> label="侧面"
{EXERCISES.map((item) => ( value={uploadState.side}
<ExerciseItem key={item.id} exercise={item} />) onPickCamera={() => requestPermissionAndPick('camera', 'side')}
)} onPickLibrary={() => requestPermissionAndPick('library', 'side')}
</View> samples={SAMPLES.side}
/>
<UploadTile
label="背面"
value={uploadState.back}
onPickCamera={() => requestPermissionAndPick('camera', 'back')}
onPickLibrary={() => requestPermissionAndPick('library', 'back')}
samples={SAMPLES.back}
/>
</ScrollView> </ScrollView>
{/* Bottom CTA */} {/* Bottom CTA */}
<View style={[styles.bottomCtaWrap, { paddingBottom: insets.bottom + 10 }]}> <View style={[styles.bottomCtaWrap, { paddingBottom: insets.bottom + 10 }]}>
<TouchableOpacity <TouchableOpacity
activeOpacity={0.9} disabled={!canStart}
onPress={() => { }} activeOpacity={1}
style={[styles.bottomCta, { backgroundColor: theme.primary }]} onPress={handleStart}
style={[
styles.bottomCta,
{ backgroundColor: canStart ? theme.primary : theme.neutral300 },
]}
> >
<Text style={styles.bottomCtaText}>Lets Workout</Text> <Text style={[styles.bottomCtaText, { color: canStart ? theme.onPrimary : theme.textMuted }]}>
{canStart ? '开始测评' : '请先完成三视角上传'}
</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>
); );
} }
function StatCard({ function UploadTile({
icon,
label, label,
value, value,
onPickCamera,
onPickLibrary,
samples,
}: { }: {
icon: React.ReactNode;
label: string; label: string;
value: string; value?: string | null;
onPickCamera: () => void;
onPickLibrary: () => void;
samples: Sample[];
}) { }) {
return ( return (
<View style={styles.statCard}> <View style={styles.section}>
<View style={styles.statIconWrap}>{icon}</View> <View style={styles.sectionHeader}>
<View style={{ marginLeft: 8 }}> <Text style={styles.sectionTitle}>{label}</Text>
<Text style={styles.statLabel}>{label}</Text> {value ? (
<Text style={styles.statValue}>{value}</Text> <Text style={styles.retakeHint}></Text>
) : (
<Text style={styles.retakeHint}></Text>
)}
</View> </View>
</View>
);
}
function ExerciseItem({ exercise }: { exercise: Exercise }) { <TouchableOpacity
return ( activeOpacity={0.95}
<View style={styles.exerciseItem}> onLongPress={onPickLibrary}
<Image source={{ uri: exercise.imageUri }} style={styles.exerciseThumb} /> onPress={onPickCamera}
<View style={{ flex: 1, marginHorizontal: 12 }}> style={styles.uploader}
<Text style={styles.exerciseTitle}>{exercise.title}</Text> >
<Text style={styles.exerciseDuration}>{exercise.duration}</Text> {value ? (
</View> <Image source={{ uri: value }} style={styles.preview} />
<TouchableOpacity style={styles.exercisePlayButton}> ) : (
<Ionicons name="play" size={18} color="#BBF246" /> <View style={styles.placeholder}>
<View style={styles.plusBadge}>
<Ionicons name="camera" size={16} color="#192126" />
</View>
<Text style={styles.placeholderTitle}></Text>
<Text style={styles.placeholderDesc}></Text>
</View>
)}
</TouchableOpacity> </TouchableOpacity>
<BlurView intensity={18} tint="dark" style={styles.sampleBox}>
<Text style={styles.sampleTitle}></Text>
<View style={styles.sampleRow}>
{samples.map((s, idx) => (
<View key={idx} style={styles.sampleItem}>
<Image source={{ uri: s.uri }} style={styles.sampleImg} />
<View style={[styles.sampleTag, { backgroundColor: s.correct ? '#2BCC7F' : '#E24D4D' }]}>
<Text style={styles.sampleTagText}>{s.correct ? '正确示范' : '错误示范'}</Text>
</View>
</View>
))}
</View>
</BlurView>
</View> </View>
); );
} }
@@ -183,6 +333,57 @@ const styles = StyleSheet.create({
screen: { screen: {
flex: 1, flex: 1,
}, },
permBanner: {
marginTop: 12,
marginHorizontal: 16,
padding: 14,
borderRadius: 16,
backgroundColor: 'rgba(255,255,255,0.04)'
},
permTitle: {
color: '#ECEDEE',
fontSize: 16,
fontWeight: '700',
},
permDesc: {
color: 'rgba(255,255,255,0.75)',
marginTop: 6,
fontSize: 13,
},
permActions: {
flexDirection: 'row',
gap: 10,
marginTop: 10,
},
permPrimary: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 14,
height: 40,
borderRadius: 12,
backgroundColor: '#BBF246',
},
permPrimaryText: {
color: '#192126',
fontSize: 14,
fontWeight: '800',
},
permSecondary: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 14,
height: 40,
borderRadius: 12,
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.18)',
},
permSecondaryText: {
color: 'rgba(255,255,255,0.85)',
fontSize: 14,
fontWeight: '700',
},
header: { header: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
@@ -202,73 +403,13 @@ const styles = StyleSheet.create({
color: '#ECEDEE', color: '#ECEDEE',
fontWeight: '700', fontWeight: '700',
}, },
heroContainer: { introBox: {
marginTop: 14, marginTop: 12,
marginHorizontal: 16,
},
heroImage: {
height: 260,
borderRadius: 28,
overflow: 'hidden',
justifyContent: 'flex-end',
},
heroOverlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0,0,0,0.18)',
borderRadius: 28,
},
statsFloating: {
position: 'absolute',
left: 22,
right: 22,
bottom: -26,
height: 72,
borderRadius: 20,
backgroundColor: '#1E262C',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-around',
paddingHorizontal: 14,
shadowColor: '#000',
shadowOpacity: 0.3,
shadowRadius: 10,
shadowOffset: { width: 0, height: 6 },
elevation: 6,
},
divider: {
width: 1,
height: 36,
backgroundColor: 'rgba(255,255,255,0.08)',
},
statCard: {
flexDirection: 'row',
alignItems: 'center',
},
statIconWrap: {
width: 36,
height: 36,
borderRadius: 12,
backgroundColor: '#BBF246',
alignItems: 'center',
justifyContent: 'center',
},
statLabel: {
color: 'rgba(255,255,255,0.75)',
fontSize: 12,
marginBottom: 2,
},
statValue: {
color: '#ECEDEE',
fontSize: 16,
fontWeight: '700',
},
contentSection: {
marginTop: 46,
paddingHorizontal: 20, paddingHorizontal: 20,
gap: 12, gap: 10,
}, },
title: { title: {
fontSize: 28, fontSize: 26,
color: '#ECEDEE', color: '#ECEDEE',
fontWeight: '800', fontWeight: '800',
}, },
@@ -277,53 +418,98 @@ const styles = StyleSheet.create({
lineHeight: 22, lineHeight: 22,
color: 'rgba(255,255,255,0.75)', color: 'rgba(255,255,255,0.75)',
}, },
roundsHeader: { section: {
marginTop: 18, marginTop: 16,
paddingHorizontal: 20, paddingHorizontal: 16,
gap: 12,
},
sectionHeader: {
paddingHorizontal: 4,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
}, },
roundsTitle: { sectionTitle: {
color: '#ECEDEE', color: '#ECEDEE',
fontSize: 22, fontSize: 18,
fontWeight: '700', fontWeight: '700',
}, },
roundsCount: { retakeHint: {
color: 'rgba(255,255,255,0.6)', color: 'rgba(255,255,255,0.55)',
fontSize: 16,
},
exerciseItem: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#1E262C',
padding: 12,
borderRadius: 18,
},
exerciseThumb: {
width: 56,
height: 56,
borderRadius: 12,
},
exerciseTitle: {
color: '#ECEDEE',
fontSize: 16,
fontWeight: '600',
},
exerciseDuration: {
color: 'rgba(255,255,255,0.6)',
marginTop: 4,
fontSize: 13, fontSize: 13,
}, },
exercisePlayButton: { uploader: {
height: 220,
borderRadius: 18,
borderWidth: 1,
borderStyle: 'dashed',
borderColor: 'rgba(255,255,255,0.18)',
backgroundColor: '#1E262C',
overflow: 'hidden',
},
preview: {
width: '100%',
height: '100%',
},
placeholder: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
gap: 8,
},
plusBadge: {
width: 36, width: 36,
height: 36, height: 36,
borderRadius: 18, borderRadius: 18,
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.2)',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
backgroundColor: '#192126', backgroundColor: '#BBF246',
},
placeholderTitle: {
color: '#ECEDEE',
fontSize: 16,
fontWeight: '700',
},
placeholderDesc: {
color: 'rgba(255,255,255,0.65)',
fontSize: 12,
},
sampleBox: {
marginTop: 8,
borderRadius: 16,
padding: 12,
backgroundColor: 'rgba(255,255,255,0.04)',
},
sampleTitle: {
color: 'rgba(255,255,255,0.8)',
fontSize: 14,
marginBottom: 8,
fontWeight: '600',
},
sampleRow: {
flexDirection: 'row',
gap: 10,
},
sampleItem: {
flex: 1,
},
sampleImg: {
width: '100%',
height: 90,
borderRadius: 12,
backgroundColor: '#111',
},
sampleTag: {
alignSelf: 'flex-start',
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 8,
marginTop: 6,
},
sampleTagText: {
color: '#192126',
fontSize: 12,
fontWeight: '700',
}, },
bottomCtaWrap: { bottomCtaWrap: {
position: 'absolute', position: 'absolute',
@@ -338,10 +524,8 @@ const styles = StyleSheet.create({
justifyContent: 'center', justifyContent: 'center',
}, },
bottomCtaText: { bottomCtaText: {
color: '#192126',
fontSize: 18, fontSize: 18,
fontWeight: '800', fontWeight: '800',
}, },
}); });

View File

@@ -1,33 +1,103 @@
/** /**
* Below are the colors that are used in the app. The colors are defined in the light and dark mode. * 应用全局配色规范(来自设计规范图)。
* There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc. * 说明:保持原有导出结构不变,同时扩展更完整的语义令牌与原子调色板。
*/ */
const primaryColor = '#BBF246'; // 应用主题色 // 原子调色板(与设计图一致)
export const palette = {
// Primary
primary: '#BBF246',
ink: '#192126',
// Secondary / Neutrals
neutral100: '#888F92',
neutral200: '#5E6468',
neutral300: '#384046',
// Accents
purple: '#A48AED',
red: '#ED4747',
orange: '#FCC46F',
blue: '#95CCE3',
} as const;
const primaryColor = palette.primary; // 应用主题色
const tintColorLight = primaryColor; const tintColorLight = primaryColor;
const tintColorDark = '#fff'; const tintColorDark = '#FFFFFF';
export const Colors = { export const Colors = {
light: { light: {
// 基础文本/背景
text: '#11181C', text: '#11181C',
background: '#fff', textSecondary: palette.neutral300,
textMuted: palette.neutral200,
background: '#FFFFFF',
surface: '#FFFFFF',
card: '#FFFFFF',
// 品牌与可交互主色
tint: tintColorLight, tint: tintColorLight,
primary: primaryColor, primary: primaryColor,
onPrimary: palette.ink, // 与主色搭配的前景色(按钮文字/图标)
// 中性色与辅助
neutral100: palette.neutral100,
neutral200: palette.neutral200,
neutral300: palette.neutral300,
// 状态/反馈色
success: palette.primary,
warning: palette.orange,
danger: palette.red,
info: palette.blue,
accentPurple: palette.purple,
// 结构色
border: palette.neutral100 + '33', // 20% 透明度
separator: palette.neutral100 + '33',
icon: '#687076', icon: '#687076',
// Tab 相关(保持兼容)
tabIconDefault: '#687076', tabIconDefault: '#687076',
tabIconSelected: '#192126', // tab 激活时的文字/图标颜色(深色,在亮色背景上显示) tabIconSelected: palette.ink, // tab 激活时的文字/图标颜色(深色,在亮色背景上显示)
tabBarBackground: '#192126', // tab 栏背景色 tabBarBackground: palette.ink, // tab 栏背景色
tabBarActiveBackground: primaryColor, // tab 激活时的背景色 tabBarActiveBackground: primaryColor, // tab 激活时的背景色
}, },
dark: { dark: {
// 基础文本/背景
text: '#ECEDEE', text: '#ECEDEE',
textSecondary: palette.neutral100,
textMuted: '#9BA1A6',
background: '#151718', background: '#151718',
surface: '#1A1D1E',
card: '#1A1D1E',
// 品牌与可交互主色
tint: tintColorDark, tint: tintColorDark,
primary: primaryColor, primary: primaryColor,
onPrimary: palette.ink,
// 中性色与辅助
neutral100: palette.neutral100,
neutral200: palette.neutral200,
neutral300: palette.neutral300,
// 状态/反馈色
success: palette.primary,
warning: palette.orange,
danger: palette.red,
info: palette.blue,
accentPurple: palette.purple,
// 结构色
border: '#2A2F32',
separator: '#2A2F32',
icon: '#9BA1A6', icon: '#9BA1A6',
// Tab 相关(保持兼容)
tabIconDefault: '#9BA1A6', tabIconDefault: '#9BA1A6',
tabIconSelected: '#192126', // 在亮色背景上使用深色文字 tabIconSelected: palette.ink, // 在亮色背景上使用深色文字
tabBarBackground: '#192126', tabBarBackground: palette.ink,
tabBarActiveBackground: primaryColor, tabBarActiveBackground: primaryColor,
}, },
}; } as const;

View File

@@ -3,6 +3,9 @@ PODS:
- DoubleConversion (1.1.6) - DoubleConversion (1.1.6)
- EXConstants (17.1.7): - EXConstants (17.1.7):
- ExpoModulesCore - ExpoModulesCore
- EXImageLoader (5.1.0):
- ExpoModulesCore
- React-Core
- Expo (53.0.20): - Expo (53.0.20):
- DoubleConversion - DoubleConversion
- ExpoModulesCore - ExpoModulesCore
@@ -49,6 +52,8 @@ PODS:
- SDWebImageAVIFCoder (~> 0.11.0) - SDWebImageAVIFCoder (~> 0.11.0)
- SDWebImageSVGCoder (~> 1.7.0) - SDWebImageSVGCoder (~> 1.7.0)
- SDWebImageWebPCoder (~> 0.14.6) - SDWebImageWebPCoder (~> 0.14.6)
- ExpoImagePicker (16.1.4):
- ExpoModulesCore
- ExpoKeepAwake (14.1.4): - ExpoKeepAwake (14.1.4):
- ExpoModulesCore - ExpoModulesCore
- ExpoLinearGradient (14.1.5): - ExpoLinearGradient (14.1.5):
@@ -2100,6 +2105,7 @@ DEPENDENCIES:
- boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`)
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
- EXConstants (from `../node_modules/expo-constants/ios`) - EXConstants (from `../node_modules/expo-constants/ios`)
- EXImageLoader (from `../node_modules/expo-image-loader/ios`)
- Expo (from `../node_modules/expo`) - Expo (from `../node_modules/expo`)
- ExpoAsset (from `../node_modules/expo-asset/ios`) - ExpoAsset (from `../node_modules/expo-asset/ios`)
- ExpoBlur (from `../node_modules/expo-blur/ios`) - ExpoBlur (from `../node_modules/expo-blur/ios`)
@@ -2108,6 +2114,7 @@ DEPENDENCIES:
- ExpoHaptics (from `../node_modules/expo-haptics/ios`) - ExpoHaptics (from `../node_modules/expo-haptics/ios`)
- ExpoHead (from `../node_modules/expo-router/ios`) - ExpoHead (from `../node_modules/expo-router/ios`)
- ExpoImage (from `../node_modules/expo-image/ios`) - ExpoImage (from `../node_modules/expo-image/ios`)
- ExpoImagePicker (from `../node_modules/expo-image-picker/ios`)
- 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`)
@@ -2212,6 +2219,8 @@ EXTERNAL SOURCES:
:podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec"
EXConstants: EXConstants:
:path: "../node_modules/expo-constants/ios" :path: "../node_modules/expo-constants/ios"
EXImageLoader:
:path: "../node_modules/expo-image-loader/ios"
Expo: Expo:
:path: "../node_modules/expo" :path: "../node_modules/expo"
ExpoAsset: ExpoAsset:
@@ -2228,6 +2237,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/expo-router/ios" :path: "../node_modules/expo-router/ios"
ExpoImage: ExpoImage:
:path: "../node_modules/expo-image/ios" :path: "../node_modules/expo-image/ios"
ExpoImagePicker:
:path: "../node_modules/expo-image-picker/ios"
ExpoKeepAwake: ExpoKeepAwake:
:path: "../node_modules/expo-keep-awake/ios" :path: "../node_modules/expo-keep-awake/ios"
ExpoLinearGradient: ExpoLinearGradient:
@@ -2400,6 +2411,7 @@ SPEC CHECKSUMS:
boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90
DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb
EXConstants: 98bcf0f22b820f9b28f9fee55ff2daededadd2f8 EXConstants: 98bcf0f22b820f9b28f9fee55ff2daededadd2f8
EXImageLoader: 4d3d3284141f1a45006cc4d0844061c182daf7ee
Expo: a40d525c930dd1c8a158e082756ee071955baccb Expo: a40d525c930dd1c8a158e082756ee071955baccb
ExpoAsset: ef06e880126c375f580d4923fdd1cdf4ee6ee7d6 ExpoAsset: ef06e880126c375f580d4923fdd1cdf4ee6ee7d6
ExpoBlur: 3c8885b9bf9eef4309041ec87adec48b5f1986a9 ExpoBlur: 3c8885b9bf9eef4309041ec87adec48b5f1986a9
@@ -2408,6 +2420,7 @@ SPEC CHECKSUMS:
ExpoHaptics: 0ff6e0d83cd891178a306e548da1450249d54500 ExpoHaptics: 0ff6e0d83cd891178a306e548da1450249d54500
ExpoHead: a7b66cbaeeb51f4a85338d335a0f5467e29a2c90 ExpoHead: a7b66cbaeeb51f4a85338d335a0f5467e29a2c90
ExpoImage: e4102c93d1dbe99ff54b075452d1bc9d6ec21b7c ExpoImage: e4102c93d1dbe99ff54b075452d1bc9d6ec21b7c
ExpoImagePicker: 0963da31800c906e01c03e25d7c849f16ebf02a2
ExpoKeepAwake: bf0811570c8da182bfb879169437d4de298376e7 ExpoKeepAwake: bf0811570c8da182bfb879169437d4de298376e7
ExpoLinearGradient: 7734c8059972fcf691fb4330bcdf3390960a152d ExpoLinearGradient: 7734c8059972fcf691fb4330bcdf3390960a152d
ExpoLinking: d5c183998ca6ada66ff45e407e0f965b398a8902 ExpoLinking: d5c183998ca6ada66ff45e407e0f965b398a8902

View File

@@ -8,28 +8,28 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
27B482E4BA415859EA8B0372 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8732A3660B00244505674555 /* ExpoModulesProvider.swift */; }; 2C9C524987451393B76B9C7E /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 7EC44F9488C227087AA8DF97 /* PrivacyInfo.xcprivacy */; };
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; }; 3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
7F5A026795C1BF28BEBBFA4E /* libPods-digitalpilates.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FDAB65677BEFC9C3E52449E8 /* libPods-digitalpilates.a */; }; 6B6021A2D1EB466803BE19D7 /* libPods-digitalpilates.a in Frameworks */ = {isa = PBXBuildFile; fileRef = F1A078ADDB1BCB06E0DBEFDA /* libPods-digitalpilates.a */; };
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; }; BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
CBE4D0B57478826DADA47BC9 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = C59D7448330DC33ACFEECB09 /* PrivacyInfo.xcprivacy */; }; DC3BFC72D3A68C7493D5B44A /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D1B5F0EC906D7A2F599549 /* ExpoModulesProvider.swift */; };
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 */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
031BAB3982A98E78D1493449 /* Pods-digitalpilates.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-digitalpilates.debug.xcconfig"; path = "Target Support Files/Pods-digitalpilates/Pods-digitalpilates.debug.xcconfig"; sourceTree = "<group>"; };
13B07F961A680F5B00A75B9A /* digitalpilates.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = digitalpilates.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07F961A680F5B00A75B9A /* digitalpilates.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = digitalpilates.app; sourceTree = BUILT_PRODUCTS_DIR; };
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = digitalpilates/Images.xcassets; sourceTree = "<group>"; }; 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = digitalpilates/Images.xcassets; sourceTree = "<group>"; };
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = digitalpilates/Info.plist; sourceTree = "<group>"; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = digitalpilates/Info.plist; sourceTree = "<group>"; };
7398BB3C47424238F7BEF8F9 /* Pods-digitalpilates.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-digitalpilates.release.xcconfig"; path = "Target Support Files/Pods-digitalpilates/Pods-digitalpilates.release.xcconfig"; sourceTree = "<group>"; }; 4D6B8E20DD8E5677F8B2EAA1 /* Pods-digitalpilates.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-digitalpilates.debug.xcconfig"; path = "Target Support Files/Pods-digitalpilates/Pods-digitalpilates.debug.xcconfig"; sourceTree = "<group>"; };
8732A3660B00244505674555 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-digitalpilates/ExpoModulesProvider.swift"; sourceTree = "<group>"; }; 7EC44F9488C227087AA8DF97 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = digitalpilates/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
83D1B5F0EC906D7A2F599549 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-digitalpilates/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = digitalpilates/SplashScreen.storyboard; sourceTree = "<group>"; }; AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = digitalpilates/SplashScreen.storyboard; 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>"; };
C59D7448330DC33ACFEECB09 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = digitalpilates/PrivacyInfo.xcprivacy; sourceTree = "<group>"; }; EA6A757B2DE1747F7B3664B4 /* Pods-digitalpilates.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-digitalpilates.release.xcconfig"; path = "Target Support Files/Pods-digitalpilates/Pods-digitalpilates.release.xcconfig"; 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; };
F11748412D0307B40044C1D9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = digitalpilates/AppDelegate.swift; sourceTree = "<group>"; }; F11748412D0307B40044C1D9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = digitalpilates/AppDelegate.swift; sourceTree = "<group>"; };
F11748442D0722820044C1D9 /* digitalpilates-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "digitalpilates-Bridging-Header.h"; path = "digitalpilates/digitalpilates-Bridging-Header.h"; sourceTree = "<group>"; }; F11748442D0722820044C1D9 /* digitalpilates-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "digitalpilates-Bridging-Header.h"; path = "digitalpilates/digitalpilates-Bridging-Header.h"; sourceTree = "<group>"; };
FDAB65677BEFC9C3E52449E8 /* libPods-digitalpilates.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-digitalpilates.a"; sourceTree = BUILT_PRODUCTS_DIR; }; F1A078ADDB1BCB06E0DBEFDA /* libPods-digitalpilates.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-digitalpilates.a"; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@@ -37,7 +37,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
7F5A026795C1BF28BEBBFA4E /* libPods-digitalpilates.a in Frameworks */, 6B6021A2D1EB466803BE19D7 /* libPods-digitalpilates.a in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -53,7 +53,7 @@
13B07FB51A68108700A75B9A /* Images.xcassets */, 13B07FB51A68108700A75B9A /* Images.xcassets */,
13B07FB61A68108700A75B9A /* Info.plist */, 13B07FB61A68108700A75B9A /* Info.plist */,
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */, AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */,
C59D7448330DC33ACFEECB09 /* PrivacyInfo.xcprivacy */, 7EC44F9488C227087AA8DF97 /* PrivacyInfo.xcprivacy */,
); );
name = digitalpilates; name = digitalpilates;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -62,17 +62,19 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
ED297162215061F000B7C4FE /* JavaScriptCore.framework */, ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
FDAB65677BEFC9C3E52449E8 /* libPods-digitalpilates.a */, F1A078ADDB1BCB06E0DBEFDA /* libPods-digitalpilates.a */,
); );
name = Frameworks; name = Frameworks;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
7B79F82457416491183F46D6 /* digitalpilates */ = { 3EE8D66219D64F4A63E8298D /* Pods */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
8732A3660B00244505674555 /* ExpoModulesProvider.swift */, 4D6B8E20DD8E5677F8B2EAA1 /* Pods-digitalpilates.debug.xcconfig */,
EA6A757B2DE1747F7B3664B4 /* Pods-digitalpilates.release.xcconfig */,
); );
name = digitalpilates; name = Pods;
path = Pods;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
832341AE1AAA6A7D00B99B32 /* Libraries */ = { 832341AE1AAA6A7D00B99B32 /* Libraries */ = {
@@ -89,8 +91,8 @@
832341AE1AAA6A7D00B99B32 /* Libraries */, 832341AE1AAA6A7D00B99B32 /* Libraries */,
83CBBA001A601CBA00E9B192 /* Products */, 83CBBA001A601CBA00E9B192 /* Products */,
2D16E6871FA4F8E400B85C8A /* Frameworks */, 2D16E6871FA4F8E400B85C8A /* Frameworks */,
8D14961AE80832AA51F9FFA3 /* Pods */, 3EE8D66219D64F4A63E8298D /* Pods */,
F7E6B913F90880BFADBE192E /* ExpoModulesProviders */, F899CC3CCA86CFEC0C4F53F7 /* ExpoModulesProviders */,
); );
indentWidth = 2; indentWidth = 2;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -105,15 +107,6 @@
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
8D14961AE80832AA51F9FFA3 /* Pods */ = {
isa = PBXGroup;
children = (
031BAB3982A98E78D1493449 /* Pods-digitalpilates.debug.xcconfig */,
7398BB3C47424238F7BEF8F9 /* Pods-digitalpilates.release.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
};
BB2F792B24A3F905000567C9 /* Supporting */ = { BB2F792B24A3F905000567C9 /* Supporting */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -123,10 +116,18 @@
path = digitalpilates/Supporting; path = digitalpilates/Supporting;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
F7E6B913F90880BFADBE192E /* ExpoModulesProviders */ = { DFAD2B7142CEC38E9ED66053 /* digitalpilates */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
7B79F82457416491183F46D6 /* digitalpilates */, 83D1B5F0EC906D7A2F599549 /* ExpoModulesProvider.swift */,
);
name = digitalpilates;
sourceTree = "<group>";
};
F899CC3CCA86CFEC0C4F53F7 /* ExpoModulesProviders */ = {
isa = PBXGroup;
children = (
DFAD2B7142CEC38E9ED66053 /* digitalpilates */,
); );
name = ExpoModulesProviders; name = ExpoModulesProviders;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -139,13 +140,13 @@
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "digitalpilates" */; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "digitalpilates" */;
buildPhases = ( buildPhases = (
08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */, 08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */,
E959B4C3D4071EECDD504F2E /* [Expo] Configure project */, 60F566376E07CDAA8138E40B /* [Expo] Configure project */,
13B07F871A680F5B00A75B9A /* Sources */, 13B07F871A680F5B00A75B9A /* Sources */,
13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8C1A680F5B00A75B9A /* Frameworks */,
13B07F8E1A680F5B00A75B9A /* Resources */, 13B07F8E1A680F5B00A75B9A /* Resources */,
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */, 800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */,
679509E8E2F87FD68CDC2915 /* [CP] Embed Pods Frameworks */, 761236F3114550442BC2DA44 /* [CP] Embed Pods Frameworks */,
); );
buildRules = ( buildRules = (
); );
@@ -195,7 +196,7 @@
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */, BB2F792D24A3F905000567C9 /* Expo.plist in Resources */,
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */, 3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */,
CBE4D0B57478826DADA47BC9 /* PrivacyInfo.xcprivacy in Resources */, 2C9C524987451393B76B9C7E /* PrivacyInfo.xcprivacy in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -239,7 +240,26 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
679509E8E2F87FD68CDC2915 /* [CP] Embed Pods Frameworks */ = { 60F566376E07CDAA8138E40B /* [Expo] Configure project */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "[Expo] Configure project";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-digitalpilates/expo-configure-project.sh\"\n";
};
761236F3114550442BC2DA44 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
@@ -295,25 +315,6 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-digitalpilates/Pods-digitalpilates-resources.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-digitalpilates/Pods-digitalpilates-resources.sh\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
E959B4C3D4071EECDD504F2E /* [Expo] Configure project */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "[Expo] Configure project";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-digitalpilates/expo-configure-project.sh\"\n";
};
/* End PBXShellScriptBuildPhase section */ /* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */
@@ -322,7 +323,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */, F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */,
27B482E4BA415859EA8B0372 /* ExpoModulesProvider.swift in Sources */, DC3BFC72D3A68C7493D5B44A /* ExpoModulesProvider.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -331,13 +332,12 @@
/* Begin XCBuildConfiguration section */ /* Begin XCBuildConfiguration section */
13B07F941A680F5B00A75B9A /* Debug */ = { 13B07F941A680F5B00A75B9A /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 031BAB3982A98E78D1493449 /* Pods-digitalpilates.debug.xcconfig */; baseConfigurationReference = 4D6B8E20DD8E5677F8B2EAA1 /* Pods-digitalpilates.debug.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = digitalpilates/digitalpilates.entitlements; CODE_SIGN_ENTITLEMENTS = digitalpilates/digitalpilates.entitlements;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 756WVXJ6MT;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
GCC_PREPROCESSOR_DEFINITIONS = ( GCC_PREPROCESSOR_DEFINITIONS = (
"$(inherited)", "$(inherited)",
@@ -368,13 +368,12 @@
}; };
13B07F951A680F5B00A75B9A /* Release */ = { 13B07F951A680F5B00A75B9A /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 7398BB3C47424238F7BEF8F9 /* Pods-digitalpilates.release.xcconfig */; baseConfigurationReference = EA6A757B2DE1747F7B3664B4 /* Pods-digitalpilates.release.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = digitalpilates/digitalpilates.entitlements; CODE_SIGN_ENTITLEMENTS = digitalpilates/digitalpilates.entitlements;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 756WVXJ6MT;
INFOPLIST_FILE = digitalpilates/Info.plist; INFOPLIST_FILE = digitalpilates/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.1; IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (

View File

@@ -1,86 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true/>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>digital-pilates</string> <string>digital-pilates</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string> <string>$(PRODUCT_NAME)</string>
<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.0</string> <string>1.0.0</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>
<array> <array>
<dict> <dict>
<key>CFBundleURLSchemes</key> <key>CFBundleURLSchemes</key>
<array> <array>
<string>digitalpilates</string> <string>digitalpilates</string>
<string>digital-pilates</string> <string>com.anonymous.digitalpilates</string>
</array> </array>
</dict> </dict>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1</string> <string>1</string>
<key>LSMinimumSystemVersion</key> <key>ITSAppUsesNonExemptEncryption</key>
<string>12.0</string> <false/>
<key>LSRequiresIPhoneOS</key> <key>LSMinimumSystemVersion</key>
<true/> <string>12.0</string>
<key>NSAppTransportSecurity</key> <key>LSRequiresIPhoneOS</key>
<dict> <true/>
<key>NSAllowsArbitraryLoads</key> <key>NSAppTransportSecurity</key>
<false/> <dict>
<key>NSAllowsLocalNetworking</key> <key>NSAllowsArbitraryLoads</key>
<true/> <false/>
</dict> <key>NSAllowsLocalNetworking</key>
<key>NSHealthShareUsageDescription</key> <true/>
<string>应用需要访问您的健康数据(步数与能量消耗)以展示运动统计。</string> </dict>
<key>NSHealthUpdateUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to update health info</string> <string>应用需要使用相机以拍摄您的体态照片用于AI测评。</string>
<key>NSUserActivityTypes</key> <key>NSHealthShareUsageDescription</key>
<array> <string>应用需要访问您的健康数据(步数与能量消耗)以展示运动统计。</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string> <key>NSHealthUpdateUsageDescription</key>
</array> <string>Allow $(PRODUCT_NAME) to update health info</string>
<key>UILaunchStoryboardName</key> <key>NSPhotoLibraryAddUsageDescription</key>
<string>SplashScreen</string> <string>应用需要写入相册以保存拍摄的体态照片(可选)。</string>
<key>UIRequiredDeviceCapabilities</key> <key>NSPhotoLibraryUsageDescription</key>
<array> <string>应用需要访问相册以选择您的体态照片用于AI测评。</string>
<string>arm64</string> <key>NSUserActivityTypes</key>
</array> <array>
<key>UIRequiresFullScreen</key> <string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
<false/> </array>
<key>UIStatusBarStyle</key> <key>UILaunchStoryboardName</key>
<string>UIStatusBarStyleDefault</string> <string>SplashScreen</string>
<key>UISupportedInterfaceOrientations</key> <key>UIRequiredDeviceCapabilities</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>arm64</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string> </array>
</array> <key>UIRequiresFullScreen</key>
<key>UISupportedInterfaceOrientations~ipad</key> <false/>
<array> <key>UIStatusBarStyle</key>
<string>UIInterfaceOrientationPortrait</string> <string>UIStatusBarStyleDefault</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string> <key>UISupportedInterfaceOrientations</key>
<string>UIInterfaceOrientationLandscapeLeft</string> <array>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationPortrait</string>
</array> <string>UIInterfaceOrientationPortraitUpsideDown</string>
<key>UIUserInterfaceStyle</key> </array>
<string>Automatic</string> <key>UISupportedInterfaceOrientations~ipad</key>
<key>UIViewControllerBasedStatusBarAppearance</key> <array>
<false/> <string>UIInterfaceOrientationPortrait</string>
<!-- Export compliance: App does not use non-exempt encryption --> <string>UIInterfaceOrientationPortraitUpsideDown</string>
<key>ITSAppUsesNonExemptEncryption</key> <string>UIInterfaceOrientationLandscapeLeft</string>
<false/> <string>UIInterfaceOrientationLandscapeRight</string>
</dict> </array>
</plist> <key>UIUserInterfaceStyle</key>
<string>Automatic</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>

22
package-lock.json generated
View File

@@ -20,6 +20,7 @@
"expo-font": "~13.3.2", "expo-font": "~13.3.2",
"expo-haptics": "~14.1.4", "expo-haptics": "~14.1.4",
"expo-image": "~2.4.0", "expo-image": "~2.4.0",
"expo-image-picker": "~16.1.4",
"expo-linear-gradient": "^14.1.5", "expo-linear-gradient": "^14.1.5",
"expo-linking": "~7.1.7", "expo-linking": "~7.1.7",
"expo-router": "~5.1.4", "expo-router": "~5.1.4",
@@ -6301,6 +6302,27 @@
} }
} }
}, },
"node_modules/expo-image-loader": {
"version": "5.1.0",
"resolved": "https://mirrors.tencent.com/npm/expo-image-loader/-/expo-image-loader-5.1.0.tgz",
"integrity": "sha512-sEBx3zDQIODWbB5JwzE7ZL5FJD+DK3LVLWBVJy6VzsqIA6nDEnSFnsnWyCfCTSvbGigMATs1lgkC2nz3Jpve1Q==",
"license": "MIT",
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-image-picker": {
"version": "16.1.4",
"resolved": "https://mirrors.tencent.com/npm/expo-image-picker/-/expo-image-picker-16.1.4.tgz",
"integrity": "sha512-bTmmxtw1AohUT+HxEBn2vYwdeOrj1CLpMXKjvi9FKSoSbpcarT4xxI0z7YyGwDGHbrJqyyic3I9TTdP2J2b4YA==",
"license": "MIT",
"dependencies": {
"expo-image-loader": "~5.1.0"
},
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-keep-awake": { "node_modules/expo-keep-awake": {
"version": "14.1.4", "version": "14.1.4",
"resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-14.1.4.tgz", "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-14.1.4.tgz",

View File

@@ -40,7 +40,8 @@
"react-native-safe-area-context": "5.4.0", "react-native-safe-area-context": "5.4.0",
"react-native-screens": "~4.11.1", "react-native-screens": "~4.11.1",
"react-native-web": "~0.20.0", "react-native-web": "~0.20.0",
"react-native-webview": "13.13.5" "react-native-webview": "13.13.5",
"expo-image-picker": "~16.1.4"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.25.2", "@babel/core": "^7.25.2",