330 lines
7.9 KiB
TypeScript
330 lines
7.9 KiB
TypeScript
import React, { memo } from 'react';
|
|
import {
|
|
View,
|
|
FlatList,
|
|
ActivityIndicator,
|
|
RefreshControl,
|
|
StyleSheet,
|
|
Image,
|
|
TouchableOpacity,
|
|
Platform
|
|
} from 'react-native';
|
|
import { VideoPlayer } from '@/components/video/video-player';
|
|
import { ThemedText } from '@/components/themed-text';
|
|
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
|
import { router } from 'expo-router';
|
|
import type { TemplateGeneration } from '@/lib/api/template-generations';
|
|
|
|
/**
|
|
* 根据文件 URL 后缀名判断媒体类型
|
|
*/
|
|
function getMediaType(url: string): 'image' | 'video' | 'unknown' {
|
|
if (!url) return '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 function ContentGallery({
|
|
generations,
|
|
isRefreshing,
|
|
onRefresh,
|
|
isLoadingMore,
|
|
hasMore,
|
|
onLoadMore,
|
|
ListHeaderComponent,
|
|
}: {
|
|
generations: TemplateGeneration[];
|
|
isRefreshing: boolean;
|
|
onRefresh: () => void;
|
|
isLoadingMore: boolean;
|
|
hasMore: boolean;
|
|
onLoadMore: () => void;
|
|
ListHeaderComponent?: React.ComponentType<any> | React.ReactElement | null;
|
|
}) {
|
|
const palette = darkPalette;
|
|
|
|
const renderItem = ({ item }: { item: TemplateGeneration }) => (
|
|
<ContentItem palette={palette} generation={item} />
|
|
);
|
|
|
|
const renderFooter = () => {
|
|
if (isLoadingMore) {
|
|
return (
|
|
<View style={[styles.loadingMoreContainer, { backgroundColor: palette.background }]}>
|
|
<ActivityIndicator size="small" color={palette.accent} />
|
|
<ThemedText style={[styles.loadingMoreText, { color: palette.textSecondary }]}>
|
|
加载更多...
|
|
</ThemedText>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
if (!hasMore && generations.length > 0) {
|
|
return (
|
|
<View style={[styles.noMoreContainer, { backgroundColor: palette.background }]}>
|
|
<ThemedText style={[styles.noMoreText, { color: palette.textSecondary }]}>
|
|
没有更多内容了
|
|
</ThemedText>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
return (
|
|
<FlatList
|
|
data={generations}
|
|
renderItem={renderItem}
|
|
ListHeaderComponent={ListHeaderComponent}
|
|
ListFooterComponent={renderFooter}
|
|
numColumns={2}
|
|
keyExtractor={(item) => item.id}
|
|
columnWrapperStyle={styles.columnWrapper}
|
|
showsVerticalScrollIndicator={false}
|
|
refreshControl={
|
|
<RefreshControl
|
|
refreshing={isRefreshing}
|
|
onRefresh={onRefresh}
|
|
tintColor={palette.accent}
|
|
colors={[palette.accent]}
|
|
/>
|
|
}
|
|
onEndReached={onLoadMore}
|
|
onEndReachedThreshold={0.5}
|
|
contentContainerStyle={styles.contentContainer}
|
|
removeClippedSubviews={true}
|
|
maxToRenderPerBatch={6}
|
|
updateCellsBatchingPeriod={50}
|
|
windowSize={10}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const ContentItem = memo(function ContentItem({
|
|
palette,
|
|
generation,
|
|
}: {
|
|
palette: any;
|
|
generation: TemplateGeneration;
|
|
}) {
|
|
const formatDate = (dateString: string) => {
|
|
const date = new Date(dateString);
|
|
return date.toLocaleDateString('en-US', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
});
|
|
};
|
|
|
|
const renderMedia = () => {
|
|
const mediaUrl = generation.resultUrl[0];
|
|
|
|
if (!mediaUrl) {
|
|
return (
|
|
<View style={[styles.mediaContainer, styles.placeholderContainer]}>
|
|
<ThemedText style={styles.placeholderText}>
|
|
{generation.type === 'VIDEO' ? '🎬' : generation.type === 'IMAGE' ? '🖼️' : '📝'}
|
|
</ThemedText>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const mediaType = getMediaType(mediaUrl);
|
|
|
|
if (mediaType === 'video') {
|
|
if (Platform.OS === 'web') {
|
|
return (
|
|
<View style={styles.mediaContainer}>
|
|
<video
|
|
src={mediaUrl}
|
|
style={{
|
|
width: '100%',
|
|
height: '100%',
|
|
objectFit: 'cover' as any,
|
|
}}
|
|
muted
|
|
playsInline
|
|
preload="metadata"
|
|
/>
|
|
</View>
|
|
);
|
|
} else {
|
|
return (
|
|
<View style={styles.mediaContainer}>
|
|
<VideoPlayer
|
|
source={{ uri: mediaUrl }}
|
|
style={styles.mediaVideo}
|
|
shouldPlay={false}
|
|
isLooping={true}
|
|
isMuted={true}
|
|
useNativeControls={false}
|
|
showPoster={true}
|
|
maxHeight={300}
|
|
/>
|
|
</View>
|
|
);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<View style={styles.mediaContainer}>
|
|
<Image
|
|
source={{ uri: mediaUrl }}
|
|
style={styles.mediaImage}
|
|
resizeMode="cover"
|
|
/>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<TouchableOpacity
|
|
style={[
|
|
styles.contentItem,
|
|
{
|
|
backgroundColor: palette.surface,
|
|
},
|
|
]}
|
|
onPress={() => router.push(`/result?generationId=${generation.id}`)}
|
|
activeOpacity={0.9}
|
|
>
|
|
{renderMedia()}
|
|
<View style={styles.contentInfo}>
|
|
<ThemedText
|
|
style={[styles.contentTitle, { color: palette.textPrimary }]}
|
|
numberOfLines={1}
|
|
>
|
|
{generation.template?.title || '未知模板'}
|
|
</ThemedText>
|
|
<View style={styles.contentMeta}>
|
|
<View style={styles.metaItem}>
|
|
<MaterialIcons name="schedule" size={14} color={palette.textSecondary} />
|
|
<ThemedText style={[styles.metaText, { color: palette.textSecondary }]}>
|
|
{formatDate(generation.createdAt)}
|
|
</ThemedText>
|
|
</View>
|
|
{generation.status !== 'completed' && (
|
|
<View style={styles.statusBadge}>
|
|
<ThemedText style={[styles.statusText, { color: palette.textSecondary }]}>
|
|
{generation.status}
|
|
</ThemedText>
|
|
</View>
|
|
)}
|
|
</View>
|
|
</View>
|
|
</TouchableOpacity>
|
|
);
|
|
});
|
|
|
|
const darkPalette = {
|
|
background: '#050505',
|
|
surface: '#121216',
|
|
border: '#1D1E24',
|
|
textPrimary: '#F6F7FA',
|
|
textSecondary: '#8E9098',
|
|
accent: '#B7FF2F',
|
|
};
|
|
|
|
const lightPalette = {
|
|
background: '#F7F8FB',
|
|
surface: '#FFFFFF',
|
|
border: '#E2E5ED',
|
|
textPrimary: '#0F1320',
|
|
textSecondary: '#5E6474',
|
|
accent: '#405CFF',
|
|
};
|
|
|
|
const styles = StyleSheet.create({
|
|
contentContainer: {
|
|
paddingBottom: 120,
|
|
paddingHorizontal: 12,
|
|
},
|
|
columnWrapper: {
|
|
justifyContent: 'space-between',
|
|
},
|
|
contentItem: {
|
|
flex: 1,
|
|
borderRadius: 12,
|
|
marginBottom: 12,
|
|
overflow: 'hidden',
|
|
marginHorizontal: 6,
|
|
},
|
|
mediaContainer: {
|
|
width: '100%',
|
|
aspectRatio: 1,
|
|
backgroundColor: '#000',
|
|
},
|
|
placeholderContainer: {
|
|
backgroundColor: '#1A1A1A',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
placeholderText: {
|
|
fontSize: 48,
|
|
},
|
|
mediaImage: {
|
|
width: '100%',
|
|
height: '100%',
|
|
},
|
|
mediaVideo: {
|
|
width: '100%',
|
|
height: '100%',
|
|
},
|
|
contentInfo: {
|
|
padding: 12,
|
|
},
|
|
contentTitle: {
|
|
fontSize: 15,
|
|
fontWeight: '600',
|
|
marginBottom: 8,
|
|
},
|
|
contentMeta: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
},
|
|
metaItem: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
marginRight: 12,
|
|
},
|
|
metaText: {
|
|
fontSize: 12,
|
|
marginLeft: 4,
|
|
},
|
|
statusBadge: {
|
|
paddingHorizontal: 8,
|
|
paddingVertical: 4,
|
|
borderRadius: 4,
|
|
backgroundColor: 'rgba(0, 0, 0, 0.1)',
|
|
},
|
|
statusText: {
|
|
fontSize: 11,
|
|
fontWeight: '600',
|
|
textTransform: 'capitalize' as const,
|
|
},
|
|
loadingMoreContainer: {
|
|
paddingVertical: 20,
|
|
alignItems: 'center',
|
|
flexDirection: 'row',
|
|
justifyContent: 'center',
|
|
gap: 12,
|
|
},
|
|
loadingMoreText: {
|
|
fontSize: 14,
|
|
},
|
|
noMoreContainer: {
|
|
paddingVertical: 20,
|
|
alignItems: 'center',
|
|
},
|
|
noMoreText: {
|
|
fontSize: 14,
|
|
},
|
|
});
|