580 lines
15 KiB
TypeScript
580 lines
15 KiB
TypeScript
import { ThemedView } from '@/components/themed-view';
|
||
import { VideoView, useVideoPlayer } from 'expo-video';
|
||
import * as MediaLibrary from 'expo-media-library';
|
||
import { Platform } from 'react-native';
|
||
import { ThemedText } from '@/components/themed-text';
|
||
|
||
// ResizeMode 兼容映射 - 移除 'stretch',expo-video 不支持
|
||
const ResizeMode = {
|
||
CONTAIN: 'contain' as const,
|
||
COVER: 'cover' as const,
|
||
STRETCH: 'fill' as const, // 使用 'fill' 替代 'stretch'
|
||
};
|
||
import { router, useLocalSearchParams } from 'expo-router';
|
||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||
import {
|
||
Copy,
|
||
Download,
|
||
Maximize2,
|
||
Plus,
|
||
X,
|
||
ArrowLeft,
|
||
} from 'lucide-react';
|
||
import React, { useEffect, useState } from 'react';
|
||
import {
|
||
Alert,
|
||
Dimensions,
|
||
Image,
|
||
ImageStyle,
|
||
Modal,
|
||
ScrollView,
|
||
StyleSheet,
|
||
StyleProp,
|
||
Text,
|
||
TouchableOpacity,
|
||
View,
|
||
ViewStyle,
|
||
} from 'react-native';
|
||
|
||
const { width: screenWidth } = Dimensions.get('window');
|
||
const HERO_HEIGHT = screenWidth * 0.58;
|
||
const DETAIL_HEIGHT = screenWidth * 0.95;
|
||
|
||
interface ResultWithTemplate {
|
||
id: string;
|
||
userId: string;
|
||
templateId: string;
|
||
type: 'TEXT' | 'IMAGE' | 'VIDEO';
|
||
resultUrl: string[];
|
||
status: 'pending' | 'running' | 'completed' | 'failed';
|
||
creditsCost?: number;
|
||
creditsTransactionId?: string;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
templateName?: string;
|
||
templateThumbnail?: string;
|
||
templateDescription?: string;
|
||
}
|
||
|
||
export default function ResultPage() {
|
||
const { id } = useLocalSearchParams<{ id: string }>();
|
||
const insets = useSafeAreaInsets();
|
||
const [result, setResult] = useState<ResultWithTemplate | null>(null);
|
||
const [fullscreenVisible, setFullscreenVisible] = useState(false);
|
||
const [fullscreenContent, setFullscreenContent] = useState<{
|
||
type: 'image' | 'video';
|
||
url: string;
|
||
} | null>(null);
|
||
const [saving, setSaving] = useState(false);
|
||
|
||
useEffect(() => {
|
||
loadResult();
|
||
}, [id]);
|
||
|
||
const loadResult = async () => {
|
||
try {
|
||
const typeParam = typeof id === 'string' ? id.split('_')[1] : 'text';
|
||
const resultId = typeof id === 'string' ? id : 'res1_text';
|
||
|
||
const getResultByType = () => {
|
||
switch (typeParam) {
|
||
case 'image':
|
||
return {
|
||
id: resultId,
|
||
userId: 'user1',
|
||
templateId: 'tpl2',
|
||
type: 'IMAGE' as const,
|
||
resultUrl: ['https://picsum.photos/800/600?random=1'],
|
||
status: 'completed' as const,
|
||
creditsCost: 20,
|
||
createdAt: new Date().toISOString(),
|
||
updatedAt: new Date().toISOString(),
|
||
templateName: 'AI图片生成',
|
||
templateThumbnail: 'https://picsum.photos/60/60?random=2',
|
||
templateDescription: '使用AI生成高质量图片',
|
||
};
|
||
case 'video':
|
||
return {
|
||
id: resultId,
|
||
userId: 'user1',
|
||
templateId: 'tpl3',
|
||
type: 'VIDEO' as const,
|
||
resultUrl: ['https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4'],
|
||
status: 'completed' as const,
|
||
creditsCost: 50,
|
||
createdAt: new Date().toISOString(),
|
||
updatedAt: new Date().toISOString(),
|
||
templateName: 'AI视频生成',
|
||
templateThumbnail: 'https://picsum.photos/60/60?random=3',
|
||
templateDescription: '基于文本生成视频内容',
|
||
};
|
||
default:
|
||
return {
|
||
id: resultId,
|
||
userId: 'user1',
|
||
templateId: 'tpl1',
|
||
type: 'TEXT' as const,
|
||
resultUrl: [
|
||
'这是一段AI生成的营销文案,专门为您的产品精心打造。它突出了产品的核心优势,采用了吸引人的语言风格,能够有效提升用户的购买欲望和品牌认知度。通过深入分析目标受众的需求和痛点,我们精心设计了这份文案,旨在最大化品牌影响力和转化效果。',
|
||
],
|
||
status: 'completed' as const,
|
||
creditsCost: 10,
|
||
createdAt: new Date().toISOString(),
|
||
updatedAt: new Date().toISOString(),
|
||
templateName: 'AI文案生成',
|
||
templateThumbnail: 'https://picsum.photos/60/60?random=1',
|
||
templateDescription: '基于关键词生成营销文案',
|
||
};
|
||
}
|
||
};
|
||
|
||
setResult(getResultByType());
|
||
} catch (error) {
|
||
Alert.alert('错误', '无法加载结果详情');
|
||
}
|
||
};
|
||
|
||
const handleRerun = () => {
|
||
router.push(`/templates/${result?.templateId}/form`);
|
||
};
|
||
|
||
const handleSaveToGallery = async () => {
|
||
if (!result || result.type === 'TEXT') return;
|
||
|
||
try {
|
||
setSaving(true);
|
||
|
||
const { status } = await MediaLibrary.requestPermissionsAsync();
|
||
if (status !== 'granted') {
|
||
Alert.alert('权限请求', '需要媒体库权限才能保存文件');
|
||
return;
|
||
}
|
||
|
||
Alert.alert('保存成功', `${result.type === 'IMAGE' ? '图片' : '视频'}已保存到相册`);
|
||
} catch (error) {
|
||
Alert.alert('保存失败', '无法保存文件,请检查网络连接');
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
};
|
||
|
||
const handleCopyText = async () => {
|
||
if (!result || result.type !== 'TEXT') return;
|
||
|
||
try {
|
||
await navigator.clipboard?.writeText?.(result.resultUrl[0]);
|
||
Alert.alert('复制成功', '文本已复制到剪贴板');
|
||
} catch (error) {
|
||
Alert.alert('复制失败', '无法复制文本');
|
||
}
|
||
};
|
||
|
||
const openFullscreen = (type: 'image' | 'video', url: string) => {
|
||
setFullscreenContent({ type, url });
|
||
setFullscreenVisible(true);
|
||
};
|
||
|
||
const handleDownloadPress = () => {
|
||
if (!result) return;
|
||
|
||
if (result.type === 'TEXT') {
|
||
handleCopyText();
|
||
return;
|
||
}
|
||
|
||
handleSaveToGallery();
|
||
};
|
||
|
||
const renderVisualMedia = (
|
||
url: string,
|
||
mediaStyles: { image: StyleProp<ImageStyle>; video: StyleProp<ViewStyle> }
|
||
) => {
|
||
if (!url) return null;
|
||
|
||
if (result?.type === 'VIDEO') {
|
||
// 简化实现:使用原生 HTML5 video 或 Image 作为回退
|
||
if (Platform.OS === 'web') {
|
||
return (
|
||
<video
|
||
src={url}
|
||
style={{
|
||
width: '100%',
|
||
height: '100%',
|
||
objectFit: 'cover' as any
|
||
}}
|
||
muted
|
||
playsInline
|
||
/>
|
||
);
|
||
}
|
||
// 原生平台使用简化的 VideoView 实现
|
||
return (
|
||
<View style={mediaStyles.video}>
|
||
<ThemedText>Video: {url}</ThemedText>
|
||
</View>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<Image
|
||
source={{ uri: url }}
|
||
style={mediaStyles.image}
|
||
resizeMode="cover"
|
||
/>
|
||
);
|
||
};
|
||
|
||
if (!result) {
|
||
return (
|
||
<ThemedView style={styles.screen}>
|
||
<View style={styles.loadingContainer}>
|
||
<Text style={styles.loadingText}>加载中...</Text>
|
||
</View>
|
||
</ThemedView>
|
||
);
|
||
}
|
||
|
||
const isTextResult = result.type === 'TEXT';
|
||
const primaryPreviewUrl = result.resultUrl[0] ?? '';
|
||
|
||
return (
|
||
<ThemedView style={styles.screen}>
|
||
<View style={[styles.safeGuard, { paddingTop: insets.top || 16 }]} />
|
||
|
||
<ScrollView
|
||
contentContainerStyle={styles.scrollContent}
|
||
showsVerticalScrollIndicator={false}
|
||
>
|
||
<View style={styles.headerRow}>
|
||
<TouchableOpacity
|
||
onPress={() => router.back()}
|
||
activeOpacity={0.7}
|
||
style={styles.backButton}
|
||
>
|
||
<ArrowLeft size={22} color="#F6F6F8" />
|
||
</TouchableOpacity>
|
||
</View>
|
||
|
||
<Text style={styles.title}>
|
||
INSEAR YOUR PRODUCT INTO AN ASMR VIDEO
|
||
</Text>
|
||
<Text style={styles.subtitle}>
|
||
ASMR-Add-On keeps your original image the same while seamlessly adding our uploaded product into the ASMR scene.
|
||
</Text>
|
||
|
||
<View style={styles.heroCard}>
|
||
{isTextResult ? (
|
||
<ScrollView style={styles.textScroll} showsVerticalScrollIndicator={false}>
|
||
<Text style={styles.generatedText}>{primaryPreviewUrl}</Text>
|
||
</ScrollView>
|
||
) : (
|
||
<>
|
||
{renderVisualMedia(primaryPreviewUrl, {
|
||
image: styles.heroMedia,
|
||
video: styles.heroMedia,
|
||
})}
|
||
{result.templateThumbnail && (
|
||
<View style={styles.referencePreview}>
|
||
<Image
|
||
source={{ uri: result.templateThumbnail }}
|
||
style={styles.referenceImage}
|
||
/>
|
||
</View>
|
||
)}
|
||
</>
|
||
)}
|
||
</View>
|
||
|
||
{!isTextResult && (
|
||
<TouchableOpacity
|
||
activeOpacity={0.85}
|
||
style={styles.detailCard}
|
||
onPress={() => openFullscreen(result.type === 'VIDEO' ? 'video' : 'image', primaryPreviewUrl)}
|
||
>
|
||
<View style={styles.expandIcon}>
|
||
<Maximize2 size={18} color="#F5F5F7" />
|
||
</View>
|
||
<View style={styles.detailMediaWrapper}>
|
||
{renderVisualMedia(primaryPreviewUrl, {
|
||
image: styles.detailMedia,
|
||
video: styles.detailMedia,
|
||
})}
|
||
</View>
|
||
</TouchableOpacity>
|
||
)}
|
||
</ScrollView>
|
||
|
||
<Modal
|
||
visible={fullscreenVisible}
|
||
transparent={false}
|
||
animationType="fade"
|
||
onRequestClose={() => setFullscreenVisible(false)}
|
||
>
|
||
<View style={styles.fullscreenContainer}>
|
||
<TouchableOpacity
|
||
style={styles.closeButton}
|
||
onPress={() => setFullscreenVisible(false)}
|
||
>
|
||
<X size={30} color="#FFFFFF" />
|
||
</TouchableOpacity>
|
||
|
||
{fullscreenContent && (
|
||
<View style={styles.fullscreenMedia}>
|
||
{fullscreenContent.type === 'image' ? (
|
||
<Image
|
||
source={{ uri: fullscreenContent.url }}
|
||
style={styles.fullscreenMediaContent}
|
||
resizeMode="contain"
|
||
/>
|
||
) : (
|
||
<>
|
||
{Platform.OS === 'web' ? (
|
||
<video
|
||
src={fullscreenContent.url}
|
||
style={styles.fullscreenMediaContent as any}
|
||
autoPlay
|
||
controls
|
||
/>
|
||
) : (
|
||
<View style={styles.fullscreenMediaContent}>
|
||
<ThemedText>Video Player</ThemedText>
|
||
</View>
|
||
)}
|
||
</>
|
||
)}
|
||
</View>
|
||
)}
|
||
</View>
|
||
</Modal>
|
||
|
||
<View
|
||
style={[
|
||
styles.bottomBar,
|
||
{
|
||
paddingBottom: Math.max(insets.bottom, 20),
|
||
},
|
||
]}
|
||
>
|
||
<TouchableOpacity
|
||
activeOpacity={0.8}
|
||
style={styles.secondaryAction}
|
||
onPress={handleRerun}
|
||
>
|
||
<Plus size={20} color="#F6F6F8" />
|
||
<Text style={styles.secondaryText}>Regenerate</Text>
|
||
</TouchableOpacity>
|
||
|
||
<TouchableOpacity
|
||
activeOpacity={0.85}
|
||
style={[
|
||
styles.primaryAction,
|
||
saving && { opacity: 0.7 },
|
||
]}
|
||
onPress={handleDownloadPress}
|
||
disabled={saving}
|
||
>
|
||
{isTextResult ? (
|
||
<Copy size={20} color="#1C1C1E" />
|
||
) : (
|
||
<Download size={20} color="#1C1C1E" />
|
||
)}
|
||
<Text style={styles.primaryText}>
|
||
{isTextResult ? 'Copy Text' : 'Download'}
|
||
</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
</ThemedView>
|
||
);
|
||
}
|
||
|
||
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: {
|
||
paddingHorizontal: 24,
|
||
paddingBottom: 160,
|
||
},
|
||
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,
|
||
borderRadius: 32,
|
||
backgroundColor: '#131317',
|
||
padding: 24,
|
||
position: 'relative',
|
||
overflow: 'hidden',
|
||
},
|
||
heroMedia: {
|
||
width: '100%',
|
||
height: HERO_HEIGHT,
|
||
borderRadius: 28,
|
||
},
|
||
textScroll: {
|
||
maxHeight: HERO_HEIGHT,
|
||
},
|
||
generatedText: {
|
||
color: '#F5F5F7',
|
||
fontSize: 16,
|
||
lineHeight: 24,
|
||
},
|
||
referencePreview: {
|
||
position: 'absolute',
|
||
bottom: 24,
|
||
left: 24,
|
||
width: 74,
|
||
height: 74,
|
||
borderRadius: 20,
|
||
padding: 4,
|
||
backgroundColor: '#050506',
|
||
borderWidth: 1,
|
||
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||
},
|
||
referenceImage: {
|
||
width: '100%',
|
||
height: '100%',
|
||
borderRadius: 16,
|
||
},
|
||
detailCard: {
|
||
marginTop: 28,
|
||
borderRadius: 28,
|
||
borderWidth: 1,
|
||
borderColor: 'rgba(255, 255, 255, 0.08)',
|
||
backgroundColor: '#101012',
|
||
padding: 20,
|
||
position: 'relative',
|
||
},
|
||
detailMediaWrapper: {
|
||
borderRadius: 24,
|
||
overflow: 'hidden',
|
||
},
|
||
detailMedia: {
|
||
width: '100%',
|
||
height: DETAIL_HEIGHT,
|
||
},
|
||
expandIcon: {
|
||
position: 'absolute',
|
||
right: 20,
|
||
top: 20,
|
||
width: 40,
|
||
height: 40,
|
||
borderRadius: 20,
|
||
backgroundColor: 'rgba(24, 24, 28, 0.86)',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
zIndex: 1,
|
||
},
|
||
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,
|
||
},
|
||
});
|