feat: 添加相机和相册权限请求功能
- 在 AI 体态评估页面中集成相机和相册权限请求逻辑 - 更新 app.json 和 Info.plist,添加相应的权限说明 - 修改布局以支持照片上传功能,用户可上传正面、侧面和背面照片 - 更新 package.json 和 package-lock.json,添加 expo-image-picker 依赖
This commit is contained in:
@@ -4,9 +4,13 @@ import React from 'react';
|
||||
import { Text, TouchableOpacity, View } from 'react-native';
|
||||
|
||||
import { IconSymbol } from '@/components/ui/IconSymbol';
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { TAB_BAR_BOTTOM_OFFSET, TAB_BAR_HEIGHT } from '@/constants/TabBar';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
|
||||
export default function TabLayout() {
|
||||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||
const colorTokens = Colors[theme];
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
@@ -19,9 +23,9 @@ export default function TabLayout() {
|
||||
|
||||
return {
|
||||
headerShown: false,
|
||||
tabBarActiveTintColor: '#192126',
|
||||
tabBarActiveTintColor: colorTokens.tabIconSelected,
|
||||
tabBarButton: (props) => {
|
||||
const { children, onPress } = props;
|
||||
const { onPress } = props;
|
||||
|
||||
const handlePress = (event: any) => {
|
||||
if (process.env.EXPO_OS === 'ios') {
|
||||
@@ -30,9 +34,28 @@ export default function TabLayout() {
|
||||
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 (
|
||||
<TouchableOpacity
|
||||
onPress={handlePress}
|
||||
accessibilityRole="button"
|
||||
style={{
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
@@ -41,12 +64,32 @@ export default function TabLayout() {
|
||||
marginHorizontal: 6,
|
||||
marginVertical: 10,
|
||||
borderRadius: 25,
|
||||
backgroundColor: isSelected ? '#BBF246' : 'transparent',
|
||||
paddingHorizontal: isSelected ? 16 : 8,
|
||||
backgroundColor: isSelected ? colorTokens.tabBarActiveBackground : 'transparent',
|
||||
paddingHorizontal: isSelected ? 16 : 10,
|
||||
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>
|
||||
);
|
||||
},
|
||||
@@ -55,7 +98,7 @@ export default function TabLayout() {
|
||||
bottom: TAB_BAR_BOTTOM_OFFSET,
|
||||
height: TAB_BAR_HEIGHT,
|
||||
borderRadius: 34,
|
||||
backgroundColor: '#192126',
|
||||
backgroundColor: colorTokens.tabBarBackground,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.2,
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { BlurView } from 'expo-blur';
|
||||
import * as ImagePicker from 'expo-image-picker';
|
||||
import { useRouter } from 'expo-router';
|
||||
import React from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
Image,
|
||||
ImageBackground,
|
||||
Linking,
|
||||
Platform,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
@@ -14,49 +18,157 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
import { Colors } from '@/constants/Colors';
|
||||
|
||||
type Exercise = {
|
||||
id: string;
|
||||
title: string;
|
||||
duration: string;
|
||||
imageUri: string;
|
||||
type PoseView = 'front' | 'side' | 'back';
|
||||
|
||||
type UploadState = {
|
||||
front?: string | null;
|
||||
side?: string | null;
|
||||
back?: string | null;
|
||||
};
|
||||
|
||||
const EXERCISES: Exercise[] = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Jumping Jacks',
|
||||
duration: '00:30',
|
||||
imageUri:
|
||||
'https://images.unsplash.com/photo-1546483875-ad9014c88eba?q=80&w=400&auto=format&fit=crop',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Squats',
|
||||
duration: '00:45',
|
||||
imageUri:
|
||||
'https://images.unsplash.com/photo-1583454110551-21f2fa2f36f0?q=80&w=400&auto=format&fit=crop',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
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',
|
||||
},
|
||||
];
|
||||
type Sample = { uri: string; correct: boolean };
|
||||
|
||||
const SAMPLES: Record<PoseView, Sample[]> = {
|
||||
front: [
|
||||
{ uri: 'https://images.unsplash.com/photo-1594737625785-c6683fc87c73?w=400&q=80&auto=format', correct: true },
|
||||
{ uri: 'https://images.unsplash.com/photo-1544716278-ca5e3f4abd8c?w=400&q=80&auto=format', correct: false },
|
||||
{ uri: 'https://images.unsplash.com/photo-1571019614242-c5c5dee9f50b?w=400&q=80&auto=format', correct: false },
|
||||
],
|
||||
side: [
|
||||
{ uri: 'https://images.unsplash.com/photo-1554463529-e27854014799?w=400&q=80&auto=format', correct: true },
|
||||
{ uri: 'https://images.unsplash.com/photo-1596357395104-5bcae0b1a5eb?w=400&q=80&auto=format', correct: false },
|
||||
{ uri: 'https://images.unsplash.com/photo-1526506118085-60ce8714f8c5?w=400&q=80&auto=format', correct: false },
|
||||
],
|
||||
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 },
|
||||
{ uri: 'https://images.unsplash.com/photo-1518611012118-696072aa579a?w=400&q=80&auto=format', correct: false },
|
||||
],
|
||||
};
|
||||
|
||||
export default function AIPostureAssessmentScreen() {
|
||||
const router = useRouter();
|
||||
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 (
|
||||
<View style={[styles.screen, { backgroundColor: theme.background }]}>
|
||||
@@ -69,7 +181,7 @@ export default function AIPostureAssessmentScreen() {
|
||||
>
|
||||
<Ionicons name="chevron-back" size={24} color="#ECEDEE" />
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.headerTitle}>AI体态评估</Text>
|
||||
<Text style={styles.headerTitle}>AI体态测评</Text>
|
||||
<View style={{ width: 32 }} />
|
||||
</View>
|
||||
|
||||
@@ -77,104 +189,142 @@ export default function AIPostureAssessmentScreen() {
|
||||
contentContainerStyle={{ paddingBottom: insets.bottom + 120 }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{/* Hero */}
|
||||
<View style={styles.heroContainer}>
|
||||
<ImageBackground
|
||||
source={{
|
||||
uri:
|
||||
'https://images.unsplash.com/photo-1594737625785-c6683fc87c73?q=80&w=1200&auto=format&fit=crop',
|
||||
}}
|
||||
style={styles.heroImage}
|
||||
imageStyle={{ borderRadius: 28 }}
|
||||
>
|
||||
<View style={styles.heroOverlay} />
|
||||
</ImageBackground>
|
||||
{/* Permissions Banner (iOS 优先提示) */}
|
||||
{Platform.OS === 'ios' && (
|
||||
(cameraPerm !== 'granted' || !(libraryPerm === 'granted' || libraryAccess === 'limited')) && (
|
||||
<BlurView intensity={18} tint="dark" style={styles.permBanner}>
|
||||
<Text style={styles.permTitle}>需要相机与相册权限</Text>
|
||||
<Text style={styles.permDesc}>
|
||||
授权后可拍摄或选择三视角全身照片用于AI体态测评。
|
||||
</Text>
|
||||
<View style={styles.permActions}>
|
||||
{((cameraCanAsk ?? true) || (libraryCanAsk ?? true)) ? (
|
||||
<TouchableOpacity style={styles.permPrimary} onPress={requestAllPermissions}>
|
||||
<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 */}
|
||||
<View style={styles.statsFloating}>
|
||||
<StatCard
|
||||
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>
|
||||
{/* Intro */}
|
||||
<View style={styles.introBox}>
|
||||
<Text style={styles.title}>上传标准姿势照片</Text>
|
||||
<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>
|
||||
</View>
|
||||
|
||||
{/* Rounds header */}
|
||||
<View style={styles.roundsHeader}>
|
||||
<Text style={styles.roundsTitle}>Rounds</Text>
|
||||
<Text style={styles.roundsCount}>1/8</Text>
|
||||
</View>
|
||||
{/* Upload sections */}
|
||||
<UploadTile
|
||||
label="正面"
|
||||
value={uploadState.front}
|
||||
onPickCamera={() => requestPermissionAndPick('camera', 'front')}
|
||||
onPickLibrary={() => requestPermissionAndPick('library', 'front')}
|
||||
samples={SAMPLES.front}
|
||||
/>
|
||||
|
||||
{/* Exercise list */}
|
||||
<View style={{ gap: 14, marginHorizontal: 20 }}>
|
||||
{EXERCISES.map((item) => (
|
||||
<ExerciseItem key={item.id} exercise={item} />)
|
||||
)}
|
||||
</View>
|
||||
<UploadTile
|
||||
label="侧面"
|
||||
value={uploadState.side}
|
||||
onPickCamera={() => requestPermissionAndPick('camera', 'side')}
|
||||
onPickLibrary={() => requestPermissionAndPick('library', 'side')}
|
||||
samples={SAMPLES.side}
|
||||
/>
|
||||
|
||||
<UploadTile
|
||||
label="背面"
|
||||
value={uploadState.back}
|
||||
onPickCamera={() => requestPermissionAndPick('camera', 'back')}
|
||||
onPickLibrary={() => requestPermissionAndPick('library', 'back')}
|
||||
samples={SAMPLES.back}
|
||||
/>
|
||||
</ScrollView>
|
||||
|
||||
{/* Bottom CTA */}
|
||||
<View style={[styles.bottomCtaWrap, { paddingBottom: insets.bottom + 10 }]}>
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
onPress={() => { }}
|
||||
style={[styles.bottomCta, { backgroundColor: theme.primary }]}
|
||||
disabled={!canStart}
|
||||
activeOpacity={1}
|
||||
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>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function StatCard({
|
||||
icon,
|
||||
function UploadTile({
|
||||
label,
|
||||
value,
|
||||
onPickCamera,
|
||||
onPickLibrary,
|
||||
samples,
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
value: string;
|
||||
value?: string | null;
|
||||
onPickCamera: () => void;
|
||||
onPickLibrary: () => void;
|
||||
samples: Sample[];
|
||||
}) {
|
||||
return (
|
||||
<View style={styles.statCard}>
|
||||
<View style={styles.statIconWrap}>{icon}</View>
|
||||
<View style={{ marginLeft: 8 }}>
|
||||
<Text style={styles.statLabel}>{label}</Text>
|
||||
<Text style={styles.statValue}>{value}</Text>
|
||||
<View style={styles.section}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<Text style={styles.sectionTitle}>{label}</Text>
|
||||
{value ? (
|
||||
<Text style={styles.retakeHint}>可长按替换</Text>
|
||||
) : (
|
||||
<Text style={styles.retakeHint}>需上传此视角</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function ExerciseItem({ exercise }: { exercise: Exercise }) {
|
||||
return (
|
||||
<View style={styles.exerciseItem}>
|
||||
<Image source={{ uri: exercise.imageUri }} style={styles.exerciseThumb} />
|
||||
<View style={{ flex: 1, marginHorizontal: 12 }}>
|
||||
<Text style={styles.exerciseTitle}>{exercise.title}</Text>
|
||||
<Text style={styles.exerciseDuration}>{exercise.duration}</Text>
|
||||
</View>
|
||||
<TouchableOpacity style={styles.exercisePlayButton}>
|
||||
<Ionicons name="play" size={18} color="#BBF246" />
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.95}
|
||||
onLongPress={onPickLibrary}
|
||||
onPress={onPickCamera}
|
||||
style={styles.uploader}
|
||||
>
|
||||
{value ? (
|
||||
<Image source={{ uri: value }} style={styles.preview} />
|
||||
) : (
|
||||
<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>
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -183,6 +333,57 @@ const styles = StyleSheet.create({
|
||||
screen: {
|
||||
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: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
@@ -202,73 +403,13 @@ const styles = StyleSheet.create({
|
||||
color: '#ECEDEE',
|
||||
fontWeight: '700',
|
||||
},
|
||||
heroContainer: {
|
||||
marginTop: 14,
|
||||
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,
|
||||
introBox: {
|
||||
marginTop: 12,
|
||||
paddingHorizontal: 20,
|
||||
gap: 12,
|
||||
gap: 10,
|
||||
},
|
||||
title: {
|
||||
fontSize: 28,
|
||||
fontSize: 26,
|
||||
color: '#ECEDEE',
|
||||
fontWeight: '800',
|
||||
},
|
||||
@@ -277,53 +418,98 @@ const styles = StyleSheet.create({
|
||||
lineHeight: 22,
|
||||
color: 'rgba(255,255,255,0.75)',
|
||||
},
|
||||
roundsHeader: {
|
||||
marginTop: 18,
|
||||
paddingHorizontal: 20,
|
||||
section: {
|
||||
marginTop: 16,
|
||||
paddingHorizontal: 16,
|
||||
gap: 12,
|
||||
},
|
||||
sectionHeader: {
|
||||
paddingHorizontal: 4,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
roundsTitle: {
|
||||
sectionTitle: {
|
||||
color: '#ECEDEE',
|
||||
fontSize: 22,
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
},
|
||||
roundsCount: {
|
||||
color: 'rgba(255,255,255,0.6)',
|
||||
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,
|
||||
retakeHint: {
|
||||
color: 'rgba(255,255,255,0.55)',
|
||||
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,
|
||||
height: 36,
|
||||
borderRadius: 18,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.2)',
|
||||
alignItems: '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: {
|
||||
position: 'absolute',
|
||||
@@ -338,10 +524,8 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
},
|
||||
bottomCtaText: {
|
||||
color: '#192126',
|
||||
fontSize: 18,
|
||||
fontWeight: '800',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user