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,
},
});