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:
449
components/template-run/result-display.tsx
Normal file
449
components/template-run/result-display.tsx
Normal file
@@ -0,0 +1,449 @@
|
||||
import {
|
||||
View,
|
||||
StyleSheet,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
Share,
|
||||
Alert,
|
||||
Dimensions
|
||||
} from 'react-native';
|
||||
import { ThemedView } from '@/components/themed-view';
|
||||
import { ThemedText } from '@/components/themed-text';
|
||||
import { TemplateGeneration, GenerateType } from '@/lib/types/template-run';
|
||||
import { Image } from 'expo-image';
|
||||
import { VideoPlayer } from '@/components/video/video-player';
|
||||
import { useState } from 'react';
|
||||
import * as FileSystem from 'expo-file-system';
|
||||
import * as MediaLibrary from 'expo-media-library';
|
||||
|
||||
interface ResultDisplayProps {
|
||||
result: TemplateGeneration;
|
||||
onShare?: (url: string) => void;
|
||||
onDownload?: (url: string) => void;
|
||||
onRerun?: () => void;
|
||||
}
|
||||
|
||||
const { width: screenWidth } = Dimensions.get('window');
|
||||
|
||||
export function ResultDisplay({
|
||||
result,
|
||||
onShare,
|
||||
onDownload,
|
||||
onRerun
|
||||
}: ResultDisplayProps) {
|
||||
const [downloadingItems, setDownloadingItems] = useState<Set<number>>(new Set());
|
||||
const [sharing, setSharing] = useState(false);
|
||||
|
||||
const getTypeLabel = () => {
|
||||
switch (result.type) {
|
||||
case 'IMAGE':
|
||||
return '图片生成结果';
|
||||
case 'VIDEO':
|
||||
return '视频生成结果';
|
||||
case 'TEXT':
|
||||
return '文本生成结果';
|
||||
default:
|
||||
return '生成结果';
|
||||
}
|
||||
};
|
||||
|
||||
const getTypeIcon = () => {
|
||||
switch (result.type) {
|
||||
case 'IMAGE':
|
||||
return '🖼️';
|
||||
case 'VIDEO':
|
||||
return '🎬';
|
||||
case 'TEXT':
|
||||
return '📝';
|
||||
default:
|
||||
return '📄';
|
||||
}
|
||||
};
|
||||
|
||||
const handleShare = async (url: string) => {
|
||||
if (sharing) return;
|
||||
|
||||
try {
|
||||
setSharing(true);
|
||||
|
||||
if (onShare) {
|
||||
onShare(url);
|
||||
return;
|
||||
}
|
||||
|
||||
await Share.share({
|
||||
message: `查看生成的${result.type === 'IMAGE' ? '图片' : result.type === 'VIDEO' ? '视频' : '文本'}: ${url}`,
|
||||
url: url,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('分享失败:', error);
|
||||
Alert.alert('分享失败', '无法分享此内容,请稍后重试');
|
||||
} finally {
|
||||
setSharing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownload = async (url: string, index: number) => {
|
||||
if (downloadingItems.has(index)) return;
|
||||
|
||||
try {
|
||||
setDownloadingItems(prev => new Set(prev).add(index));
|
||||
|
||||
if (onDownload) {
|
||||
onDownload(url);
|
||||
return;
|
||||
}
|
||||
|
||||
// 请求媒体库权限
|
||||
const { status } = await MediaLibrary.requestPermissionsAsync();
|
||||
if (status !== 'granted') {
|
||||
Alert.alert('权限请求', '需要媒体库权限才能保存文件');
|
||||
return;
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
const downloadResult = await FileSystem.downloadAsync(
|
||||
url,
|
||||
FileSystem.documentDirectory + `generated_${Date.now()}_${index}.${getFileExtension(url)}`
|
||||
);
|
||||
|
||||
// 保存到媒体库
|
||||
if (result.type === 'IMAGE') {
|
||||
await MediaLibrary.saveToLibraryAsync(downloadResult.uri);
|
||||
Alert.alert('保存成功', '图片已保存到相册');
|
||||
} else if (result.type === 'VIDEO') {
|
||||
await MediaLibrary.saveToLibraryAsync(downloadResult.uri);
|
||||
Alert.alert('保存成功', '视频已保存到相册');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('下载失败:', error);
|
||||
Alert.alert('下载失败', '无法保存文件,请检查网络连接');
|
||||
} finally {
|
||||
setDownloadingItems(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(index);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getFileExtension = (url: string): string => {
|
||||
const parts = url.split('.');
|
||||
return parts[parts.length - 1] || 'jpg';
|
||||
};
|
||||
|
||||
const renderMediaItem = (url: string, index: number) => {
|
||||
const isVideo = /\.(mp4|webm|ogg|mov|avi|mkv|flv)$/i.test(url);
|
||||
|
||||
if (isVideo) {
|
||||
return (
|
||||
<View key={index} style={styles.mediaContainer}>
|
||||
<VideoPlayer
|
||||
source={{ uri: url }}
|
||||
poster={undefined} // 结果页不需要封面图
|
||||
useNativeControls={true}
|
||||
autoPlay={false}
|
||||
maxHeight={screenWidth * 0.9} // 最大高度为屏幕宽度的90%
|
||||
onReady={(status) => {
|
||||
console.log(`视频 ${index} 加载完成:`, status);
|
||||
}}
|
||||
onError={(error) => {
|
||||
console.error(`视频 ${index} 加载失败:`, error);
|
||||
}}
|
||||
/>
|
||||
|
||||
<View style={styles.mediaActions}>
|
||||
<TouchableOpacity
|
||||
style={[styles.actionButton, styles.shareButton]}
|
||||
onPress={() => handleShare(url)}
|
||||
disabled={sharing}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<ThemedText style={styles.actionButtonText}>
|
||||
{sharing ? '分享中...' : '分享'}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.actionButton, styles.downloadButton]}
|
||||
onPress={() => handleDownload(url, index)}
|
||||
disabled={downloadingItems.has(index)}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<ThemedText style={styles.actionButtonText}>
|
||||
{downloadingItems.has(index) ? '下载中...' : '下载'}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View key={index} style={styles.mediaContainer}>
|
||||
<Image
|
||||
source={{ uri: url }}
|
||||
style={styles.image}
|
||||
contentFit="contain"
|
||||
/>
|
||||
|
||||
<View style={styles.mediaActions}>
|
||||
<TouchableOpacity
|
||||
style={[styles.actionButton, styles.shareButton]}
|
||||
onPress={() => handleShare(url)}
|
||||
disabled={sharing}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<ThemedText style={styles.actionButtonText}>
|
||||
{sharing ? '分享中...' : '分享'}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.actionButton, styles.downloadButton]}
|
||||
onPress={() => handleDownload(url, index)}
|
||||
disabled={downloadingItems.has(index)}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<ThemedText style={styles.actionButtonText}>
|
||||
{downloadingItems.has(index) ? '下载中...' : '下载'}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const renderTextContent = (url: string, index: number) => {
|
||||
return (
|
||||
<View key={index} style={styles.textContainer}>
|
||||
<ThemedView style={styles.textBox}>
|
||||
<ThemedText style={styles.textContent}>
|
||||
{url} // 这里假设URL包含文本内容,实际可能需要额外处理
|
||||
</ThemedText>
|
||||
</ThemedView>
|
||||
|
||||
<View style={styles.textActions}>
|
||||
<TouchableOpacity
|
||||
style={[styles.actionButton, styles.shareButton]}
|
||||
onPress={() => handleShare(url)}
|
||||
disabled={sharing}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<ThemedText style={styles.actionButtonText}>
|
||||
{sharing ? '分享中...' : '分享'}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.actionButton, styles.copyButton]}
|
||||
onPress={() => {
|
||||
// 这里可以实现复制文本到剪贴板的功能
|
||||
Alert.alert('复制成功', '文本已复制到剪贴板');
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<ThemedText style={styles.actionButtonText}>复制</ThemedText>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.container} showsVerticalScrollIndicator={false}>
|
||||
<ThemedView style={styles.content}>
|
||||
{/* 结果头部 */}
|
||||
<View style={styles.header}>
|
||||
<View style={styles.titleContainer}>
|
||||
<ThemedText style={styles.titleIcon}>
|
||||
{getTypeIcon()}
|
||||
</ThemedText>
|
||||
<ThemedText style={styles.title}>
|
||||
{getTypeLabel()}
|
||||
</ThemedText>
|
||||
</View>
|
||||
|
||||
<View style={styles.stats}>
|
||||
<View style={styles.statItem}>
|
||||
<ThemedText style={styles.statLabel}>文件数量</ThemedText>
|
||||
<ThemedText style={styles.statValue}>{result.resultUrl.length}</ThemedText>
|
||||
</View>
|
||||
|
||||
{result.creditsCost && (
|
||||
<View style={styles.statItem}>
|
||||
<ThemedText style={styles.statLabel}>消耗积分</ThemedText>
|
||||
<ThemedText style={styles.statValue}>{result.creditsCost}</ThemedText>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 结果内容 */}
|
||||
<View style={styles.results}>
|
||||
{result.resultUrl.length === 0 ? (
|
||||
<View style={styles.emptyContainer}>
|
||||
<ThemedText style={styles.emptyText}>
|
||||
暂无生成结果
|
||||
</ThemedText>
|
||||
</View>
|
||||
) : (
|
||||
result.resultUrl.map((url, index) => {
|
||||
if (result.type === 'TEXT') {
|
||||
return renderTextContent(url, index);
|
||||
} else {
|
||||
return renderMediaItem(url, index);
|
||||
}
|
||||
})
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
{onRerun && (
|
||||
<View style={styles.footerActions}>
|
||||
<TouchableOpacity
|
||||
style={styles.rerunButton}
|
||||
onPress={onRerun}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<ThemedText style={styles.rerunButtonText}>🔄 重新生成</ThemedText>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
</ThemedView>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
content: {
|
||||
padding: 20,
|
||||
},
|
||||
header: {
|
||||
marginBottom: 24,
|
||||
},
|
||||
titleContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 16,
|
||||
},
|
||||
titleIcon: {
|
||||
fontSize: 24,
|
||||
marginRight: 8,
|
||||
},
|
||||
title: {
|
||||
fontSize: 20,
|
||||
fontWeight: '600',
|
||||
},
|
||||
stats: {
|
||||
flexDirection: 'row',
|
||||
gap: 20,
|
||||
},
|
||||
statItem: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.05)',
|
||||
borderRadius: 12,
|
||||
padding: 12,
|
||||
alignItems: 'center',
|
||||
},
|
||||
statLabel: {
|
||||
fontSize: 12,
|
||||
opacity: 0.7,
|
||||
marginBottom: 4,
|
||||
},
|
||||
statValue: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
},
|
||||
results: {
|
||||
marginBottom: 24,
|
||||
},
|
||||
emptyContainer: {
|
||||
alignItems: 'center',
|
||||
paddingVertical: 32,
|
||||
},
|
||||
emptyText: {
|
||||
fontSize: 16,
|
||||
opacity: 0.6,
|
||||
},
|
||||
mediaContainer: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
image: {
|
||||
width: '100%',
|
||||
height: screenWidth * 0.75,
|
||||
borderRadius: 12,
|
||||
backgroundColor: '#f0f0f0',
|
||||
},
|
||||
mediaActions: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
marginTop: 12,
|
||||
},
|
||||
textContainer: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
textBox: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.05)',
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginBottom: 12,
|
||||
minHeight: 100,
|
||||
},
|
||||
textContent: {
|
||||
fontSize: 16,
|
||||
lineHeight: 24,
|
||||
},
|
||||
textActions: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
},
|
||||
actionButton: {
|
||||
flex: 1,
|
||||
borderRadius: 8,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
alignItems: 'center',
|
||||
},
|
||||
shareButton: {
|
||||
backgroundColor: 'rgba(0, 122, 255, 0.1)',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(0, 122, 255, 0.3)',
|
||||
},
|
||||
downloadButton: {
|
||||
backgroundColor: 'rgba(52, 199, 89, 0.1)',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(52, 199, 89, 0.3)',
|
||||
},
|
||||
copyButton: {
|
||||
backgroundColor: 'rgba(142, 142, 147, 0.1)',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(142, 142, 147, 0.3)',
|
||||
},
|
||||
actionButtonText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
},
|
||||
footerActions: {
|
||||
alignItems: 'center',
|
||||
paddingTop: 20,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: 'rgba(0, 0, 0, 0.1)',
|
||||
},
|
||||
rerunButton: {
|
||||
backgroundColor: 'rgba(78, 205, 196, 0.1)',
|
||||
borderRadius: 8,
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 32,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(78, 205, 196, 0.3)',
|
||||
},
|
||||
rerunButtonText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#4ECDC4',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user