import { ThemedView } from '@/components/themed-view'; import { getTemplateGeneration } from '@/lib/api/template-runs'; import * as Clipboard from 'expo-clipboard'; import * as FileSystem from 'expo-file-system'; import * as MediaLibrary from 'expo-media-library'; import { router, useLocalSearchParams } from 'expo-router'; import { VideoView, useVideoPlayer } from 'expo-video'; import { ArrowLeft, Copy, Download, Maximize2, Plus, X, } from 'lucide-react'; import React, { useEffect, useState } from 'react'; import { ActivityIndicator, Alert, Dimensions, Image, Modal, Platform, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; // ResizeMode 兼容映射 - 移除 'stretch',expo-video 不支持 const ResizeMode = { CONTAIN: 'contain' as const, COVER: 'cover' as const, STRETCH: 'fill' as const, // 使用 'fill' 替代 'stretch' }; const { width: screenWidth, height: screenHeight } = Dimensions.get('window'); const HERO_HEIGHT = screenWidth * 0.58; const VIDEO_HEIGHT = screenHeight * 0.6; // 原生视频播放组件 const NativeVideoPlayer: React.FC<{ url: string; style?: any }> = ({ url, style }) => { const player = useVideoPlayer(url, player => { player.loop = false; }); return ( ); }; interface ResultWithTemplate { id: string; userId: string; templateId: string; type: 'UNKNOWN' | 'TEXT' | 'IMAGE' | 'VIDEO'; resultUrl: string[]; status: 'pending' | 'running' | 'completed' | 'failed'; creditsCost?: number; creditsTransactionId?: string; createdAt: string; updatedAt: string; template?: { id: string; title: string; titleEn: string; }; } /** * 根据文件 URL 后缀名判断媒体类型 */ function getMediaType(url: string): 'image' | 'video' | 'unknown' { const extension = url.split('.').pop()?.toLowerCase() || ''; const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg']; const videoExts = ['mp4', 'webm', 'avi', 'mov', 'mkv', 'flv', 'm4v']; if (imageExts.includes(extension)) return 'image'; if (videoExts.includes(extension)) return 'video'; return 'unknown'; } export default function ResultPage() { const { generationId } = useLocalSearchParams<{ generationId: string }>(); const insets = useSafeAreaInsets(); const [result, setResult] = useState(null); const [loading, setLoading] = useState(true); const [fullscreenVisible, setFullscreenVisible] = useState(false); const [fullscreenContent, setFullscreenContent] = useState<{ type: 'image' | 'video'; url: string; } | null>(null); const [saving, setSaving] = useState(false); const [currentMediaIndex, setCurrentMediaIndex] = useState(0); useEffect(() => { if (generationId) { loadResult(); } }, [generationId]); const loadResult = async () => { if (!generationId) return; try { setLoading(true); const response = await getTemplateGeneration(generationId); if (response.success && response.data) { setResult(response.data); } else { Alert.alert('错误', '无法加载结果详情'); } } catch (error) { console.error('Failed to load result:', error); Alert.alert('错误', '加载结果失败,请稍后重试'); } finally { setLoading(false); } }; const handleRerun = () => { router.push(`/templates/${result?.templateId}/form`); }; const handleSaveToGallery = async () => { console.log('handleSaveToGallery called'); if (!result || result.type === 'TEXT') { console.log('Skipping: no result or text type'); return; } try { setSaving(true); console.log('Platform:', Platform.OS); console.log('URLs to download:', result.resultUrl); // Web 平台处理 if (Platform.OS === 'web') { console.log('Using web download method'); for (const url of result.resultUrl) { try { console.log('Downloading:', url); const response = await fetch(url); const blob = await response.blob(); // 类型安全的处理方式 if (typeof window !== 'undefined' && window.URL && window.URL.createObjectURL) { const blobUrl = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = blobUrl; link.download = url.split('/').pop() || 'download'; document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(blobUrl); console.log('Download triggered for:', url); } // 添加延迟,避免浏览器阻止多个下载 await new Promise(resolve => setTimeout(resolve, 300)); } catch (error) { console.error('Web download failed:', url, error); } } const count = result.resultUrl.length; const mediaType = result.type === 'IMAGE' ? '图片' : '视频'; Alert.alert( '下载开始', count > 1 ? `正在下载${count}个${mediaType}` : `正在下载${mediaType}` ); return; } // 原生平台处理 console.log('Using native download method'); const { status } = await MediaLibrary.requestPermissionsAsync(); console.log('Permission status:', status); if (status !== 'granted') { Alert.alert('权限请求', '需要媒体库权限才能保存文件'); return; } // 批量下载所有媒体文件 let successCount = 0; for (const url of result.resultUrl) { try { // 生成临时文件名 const filename = url.split('/').pop() || `download_${Date.now()}`; const fileUri = `${FileSystem.documentDirectory}${filename}`; // 下载文件到本地 console.log('Downloading:', url, 'to', fileUri); const downloadResult = await FileSystem.downloadAsync(url, fileUri); if (downloadResult.status === 200) { // 保存到相册 const asset = await MediaLibrary.createAssetAsync(downloadResult.uri); console.log('Saved to library:', asset.uri); successCount++; } else { console.error('Download failed with status:', downloadResult.status); } } catch (error) { console.error('Failed to download and save:', url, error); } } const count = result.resultUrl.length; const mediaType = result.type === 'IMAGE' ? '图片' : '视频'; if (successCount === count) { Alert.alert( '保存成功', count > 1 ? `${count}个${mediaType}已保存到相册` : `${mediaType}已保存到相册` ); } else if (successCount > 0) { Alert.alert( '部分保存成功', `${successCount}/${count}个${mediaType}已保存到相册` ); } else { Alert.alert('保存失败', '无法保存文件,请检查网络连接'); } } catch (error) { console.error('Save to gallery error:', error); Alert.alert('保存失败', '无法保存文件,请稍后重试'); } finally { setSaving(false); console.log('handleSaveToGallery completed'); } }; const handleCopyText = async () => { if (!result || result.type !== 'TEXT') return; try { await Clipboard.setStringAsync(result.resultUrl[0]); Alert.alert('复制成功', '文本已复制到剪贴板'); } catch (error) { Alert.alert('复制失败', '无法复制文本'); } }; const openFullscreen = (type: 'image' | 'video', url: string) => { setFullscreenContent({ type, url }); setFullscreenVisible(true); }; const handleDownloadPress = () => { console.log('Download button pressed'); if (!result) { console.log('No result available'); return; } console.log('Result type:', result.type); console.log('Result URLs:', result.resultUrl); if (result.type === 'TEXT') { handleCopyText(); return; } handleSaveToGallery(); }; if (loading) { return ( 加载中... ); } if (!result) { return ( 未找到结果 ); } const isTextResult = result.type === 'TEXT'; const mediaUrls = result.resultUrl || []; const hasMultipleMedia = mediaUrls.length > 1; const renderMediaItem = (url: string, index: number) => { const mediaType = getMediaType(url); return ( {mediaType === 'video' ? ( Platform.OS === 'web' ? ( ); }; const handleScroll = (event: any) => { const offsetX = event.nativeEvent.contentOffset.x; const index = Math.round(offsetX / screenWidth); setCurrentMediaIndex(index); }; return ( router.back()} activeOpacity={0.7} style={styles.backButton} > {result.template?.title || '生成结果'} {result.template?.titleEn || ''} {isTextResult ? ( {mediaUrls[0]} ) : ( {mediaUrls.map((url, index) => renderMediaItem(url, index))} {hasMultipleMedia && ( {mediaUrls.map((_, index) => ( ))} )} openFullscreen( getMediaType(mediaUrls[currentMediaIndex]), mediaUrls[currentMediaIndex] ) } > )} setFullscreenVisible(false)} > setFullscreenVisible(false)} > {fullscreenContent && ( {fullscreenContent.type === 'image' ? ( ) : ( <> {Platform.OS === 'web' ? ( )} Regenerate {isTextResult ? ( ) : ( )} {isTextResult ? 'Copy Text' : 'Download'} ); } const styles = StyleSheet.create({ screen: { flex: 1, backgroundColor: '#09090B', }, loadingContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#09090B', }, loadingText: { color: '#F5F5F7', fontSize: 16, }, safeGuard: { backgroundColor: 'transparent', }, scrollContent: { paddingBottom: 160, }, contentPadding: { paddingHorizontal: 24, }, headerRow: { flexDirection: 'row', justifyContent: 'flex-start', alignItems: 'center', marginBottom: 24, }, backButton: { width: 44, height: 44, borderRadius: 22, borderWidth: 1, borderColor: 'rgba(255, 255, 255, 0.12)', justifyContent: 'center', alignItems: 'center', backgroundColor: 'rgba(32, 32, 36, 0.6)', }, title: { color: '#F5F5F7', fontSize: 22, fontWeight: '700', lineHeight: 30, marginBottom: 12, letterSpacing: 0.4, }, subtitle: { color: '#9A9AA2', fontSize: 14, lineHeight: 20, }, heroCard: { marginTop: 32, marginHorizontal: 24, borderRadius: 32, backgroundColor: '#131317', padding: 24, position: 'relative', overflow: 'hidden', }, mediaContainer: { marginTop: 32, position: 'relative', backgroundColor: '#09090B', }, carouselContent: { gap: 0, }, pagination: { flexDirection: 'row', justifyContent: 'center', alignItems: 'center', marginTop: 20, marginBottom: 12, gap: 8, }, paginationDot: { width: 8, height: 8, borderRadius: 4, backgroundColor: 'rgba(255, 255, 255, 0.3)', }, paginationDotActive: { backgroundColor: '#D9FF3F', width: 24, }, expandButton: { position: 'absolute', right: 16, top: 16, width: 44, height: 44, borderRadius: 22, backgroundColor: 'rgba(0, 0, 0, 0.6)', alignItems: 'center', justifyContent: 'center', zIndex: 1, }, textScroll: { maxHeight: HERO_HEIGHT, }, generatedText: { color: '#F5F5F7', fontSize: 16, lineHeight: 24, }, fullscreenContainer: { flex: 1, backgroundColor: '#000000', justifyContent: 'center', alignItems: 'center', }, closeButton: { position: 'absolute', top: 50, right: 20, zIndex: 1, width: 50, height: 50, justifyContent: 'center', alignItems: 'center', }, fullscreenMedia: { width: '100%', height: '100%', justifyContent: 'center', alignItems: 'center', }, fullscreenMediaContent: { width: '100%', height: '100%', }, bottomBar: { position: 'absolute', left: 0, right: 0, bottom: 0, paddingHorizontal: 24, paddingTop: 16, backgroundColor: 'rgba(6, 6, 7, 0.94)', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', }, secondaryAction: { flex: 1, height: 60, borderRadius: 30, backgroundColor: '#151519', borderWidth: 1, borderColor: 'rgba(255, 255, 255, 0.08)', marginRight: 12, justifyContent: 'center', alignItems: 'center', flexDirection: 'row', }, secondaryText: { color: '#F5F5F7', fontSize: 16, fontWeight: '600', marginLeft: 8, }, primaryAction: { flex: 1, height: 60, borderRadius: 30, backgroundColor: '#D9FF3F', justifyContent: 'center', alignItems: 'center', flexDirection: 'row', }, primaryText: { color: '#1C1C1E', fontSize: 16, fontWeight: '700', marginLeft: 10, }, });