Initial commit: Expo app with Better Auth integration

- Complete Expo React Native app setup with TypeScript
- Better Auth authentication system integration
- Secure storage implementation for session tokens
- Authentication flow with login/logout functionality
- API client configuration for backend communication
- Responsive UI components with themed styling
- Expo Router navigation setup
- Development configuration and scripts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
imeepos
2025-10-14 10:32:52 +08:00
commit 7e3f94bae3
71 changed files with 20800 additions and 0 deletions

View File

@@ -0,0 +1,235 @@
import { StyleSheet, TouchableOpacity, View } from 'react-native';
import { Image } from 'expo-image';
import { Video, ResizeMode } from 'expo-av';
import { VideoPlayer } from '@/components/video/video-player';
import { ThemedText } from '../themed-text';
import { ThemedView } from '../themed-view';
import { Template } from '@/lib/types/template';
import { useThemeColor } from '@/hooks/use-theme-color';
import { useMemo } from 'react';
import { useRouter } from 'expo-router';
interface TemplateCardProps {
template: Template;
onPress?: (template: Template) => void;
}
export function TemplateCard({ template, onPress }: TemplateCardProps) {
const router = useRouter();
const cardColor = useThemeColor({}, 'card');
const imagePlaceholderColor = useThemeColor({}, 'imagePlaceholder');
const isVideo = useMemo(() => {
return /\.(mp4|webm|ogg|mov|avi|mkv|flv)$/i.test(template.previewUrl || '');
}, [template.previewUrl]);
const getImageHeight = () => {
if (template.aspectRatio) {
const [width, height] = template.aspectRatio.split(':').map(Number);
if (width && height) {
return width > height ? 280 : 360;
}
}
return 280;
};
const mediaHeight = getImageHeight();
const handleUseTemplate = () => {
router.push(`/template/${template.id}/run`);
};
const handleCardPress = () => {
if (onPress) {
onPress(template);
} else {
// 默认行为:跳转到运行页面
handleUseTemplate();
}
};
return (
<View style={[styles.card, { backgroundColor: cardColor }]}>
<View style={styles.mediaContainer}>
<ThemedView style={[styles.mediaContent, { height: mediaHeight }]}>
{isVideo ? (
<VideoPlayer
source={{ uri: template.previewUrl }}
poster={template.coverImageUrl}
style={[styles.videoPlayer, styles.videoCover]}
resizeMode={ResizeMode.COVER}
shouldPlay={true}
isLooping={true}
isMuted={true}
useNativeControls={false}
showPoster={true}
autoPlay={true}
maxHeight={mediaHeight}
onPress={handleCardPress}
/>
) : (
<TouchableOpacity onPress={handleCardPress} activeOpacity={0.9} style={styles.imageContainer}>
<Image
source={{ uri: template.coverImageUrl }}
style={[
styles.image,
{ backgroundColor: imagePlaceholderColor },
]}
contentFit="cover"
placeholder={{ blurhash: 'L6PZfSi_.AyE_3t7t7R**0o#DgR4' }}
transition={200}
/>
</TouchableOpacity>
)}
{template.category && (
<View style={styles.watermark}>
<View style={styles.categoryBadge}>
<ThemedText style={styles.categoryText}>
{template.category.name}
</ThemedText>
</View>
</View>
)}
{template.tags.length > 0 && (
<View style={styles.tagOverlay}>
{template.tags.slice(0, 2).map((tag) => (
<View key={tag.id} style={styles.tagBadge}>
<ThemedText style={styles.tagText}>
{tag.name}
</ThemedText>
</View>
))}
</View>
)}
</ThemedView>
</View>
{/* 操作按钮区域 */}
<View style={styles.actionArea}>
<ThemedText style={styles.templateTitle} numberOfLines={1}>
{template.title}
</ThemedText>
<TouchableOpacity
style={styles.useButton}
onPress={handleUseTemplate}
activeOpacity={0.8}
>
<ThemedText style={styles.useButtonText}>使</ThemedText>
</TouchableOpacity>
</View>
</View>
);
}
const styles = StyleSheet.create({
card: {
flex: 1,
marginBottom: 14,
marginHorizontal: 5,
borderRadius: 16,
overflow: 'hidden',
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.08,
shadowRadius: 24,
elevation: 4,
},
mediaContainer: {
width: '100%',
},
mediaContent: {
position: 'relative',
width: '100%',
minHeight: 280,
},
image: {
width: '100%',
height: '100%',
},
videoPlayer: {
width: '100%',
height: '100%',
},
videoCover: {
overflow: 'hidden',
width: '100%',
height: '100%',
// React Native中object-fit对应contentFit属性但在VideoPlayer中已经处理了
},
imageContainer: {
width: '100%',
height: '100%',
},
watermark: {
position: 'absolute',
top: 12,
left: 14,
zIndex: 10,
},
categoryBadge: {
backgroundColor: 'rgba(0, 0, 0, 0.15)',
paddingHorizontal: 10,
paddingVertical: 6,
borderRadius: 100,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.2)',
},
categoryText: {
color: 'rgba(255, 255, 255, 0.85)',
fontSize: 10,
fontWeight: '400',
},
tagOverlay: {
position: 'absolute',
bottom: 16,
left: 24,
right: 24,
zIndex: 10,
flexDirection: 'row',
gap: 8,
justifyContent: 'center',
},
tagBadge: {
backgroundColor: 'rgba(0, 0, 0, 0.3)',
paddingHorizontal: 12,
paddingVertical: 8,
borderRadius: 100,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.3)',
backdropFilter: 'blur(5px)',
},
tagText: {
color: '#fff',
fontSize: 11,
fontWeight: '500',
},
actionArea: {
padding: 12,
backgroundColor: 'rgba(0, 0, 0, 0.02)',
},
templateTitle: {
fontSize: 14,
fontWeight: '600',
marginBottom: 8,
textAlign: 'center',
},
useButton: {
backgroundColor: '#4ECDC4',
borderRadius: 8,
paddingVertical: 10,
alignItems: 'center',
shadowColor: '#4ECDC4',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 8,
elevation: 3,
},
useButtonText: {
fontSize: 14,
fontWeight: '600',
color: '#fff',
},
});