Initial commit: expo-popcore project

This commit is contained in:
imeepos
2025-12-25 16:31:17 +08:00
commit 0cbb74e03a
188 changed files with 20796 additions and 0 deletions

View File

@@ -0,0 +1,310 @@
import { ThemedText } from '@/components/themed-text';
import { VideoPlayer } from '@/components/video/video-player';
import { useThemeColor } from '@/hooks/use-theme-color';
import { Template } from '@/lib/types/template';
import { Image } from 'expo-image';
import React, { useEffect, useRef, useState, useCallback } from 'react';
// ResizeMode 兼容映射
const ResizeMode = {
CONTAIN: 'contain' as const,
COVER: 'cover' as const,
STRETCH: 'fill' as const,
};
import {
ActivityIndicator,
Dimensions,
Modal,
PanResponder,
Platform,
StatusBar,
StyleSheet,
TouchableOpacity,
View,
BackHandler,
} from 'react-native';
import { isVideoTemplate, canLoadMedia, getEffectiveMediaUrl } from '@/utils/media-utils';
const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
export interface FullscreenMediaModalProps {
visible: boolean;
onClose: () => void;
currentIndex: number;
templates: Template[];
onIndexChanged: (index: number) => void;
}
export function FullscreenMediaModal({
visible,
onClose,
currentIndex,
templates,
onIndexChanged,
}: FullscreenMediaModalProps) {
const [isLoading, setIsLoading] = useState(true);
const backgroundColor = useThemeColor({}, 'background');
// 获取当前模板
const currentTemplate = templates[currentIndex];
const isCurrentVideo = currentTemplate ? isVideoTemplate(currentTemplate) : false;
const canLoadCurrentMedia = currentTemplate ? canLoadMedia(currentTemplate) : false;
// 重置状态当模态框打开/关闭时
useEffect(() => {
if (visible) {
setIsLoading(true);
// 添加备用机制3秒后自动隐藏loading防止卡住
const timer = setTimeout(() => {
setIsLoading(false);
}, 3000);
return () => clearTimeout(timer);
}
}, [visible, currentIndex]);
const markMediaLoaded = () => {
setIsLoading(false);
};
// 处理Web平台视频加载完成
const handleWebVideoLoad = () => {
setIsLoading(false);
};
// 处理媒体加载错误
const handleMediaError = (error: any) => {
console.error('媒体加载错误:', {
template: currentTemplate?.title,
mediaType: isCurrentVideo ? 'video' : 'image',
url: getEffectiveMediaUrl(currentTemplate),
error: error
});
setIsLoading(false);
};
// 媒体点击处理(直接关闭)
const handleClose = useCallback(() => {
onClose();
}, [onClose]);
const handleMediaPress = () => {
handleClose();
};
// 统一的切换处理函数
const handlePrevious = () => {
if (currentIndex > 0) {
const newIndex = currentIndex - 1;
onIndexChanged(newIndex);
}
};
const handleNext = () => {
if (currentIndex < templates.length - 1) {
const newIndex = currentIndex + 1;
onIndexChanged(newIndex);
}
};
// 创建手势处理器
const panResponder = useRef(
PanResponder.create({
onMoveShouldSetPanResponder: (_, gestureState) => {
// 只有水平移动时才响应
return Math.abs(gestureState.dx) > Math.abs(gestureState.dy) && Math.abs(gestureState.dx) > 10;
},
onPanResponderRelease: (_, gestureState) => {
const threshold = screenWidth / 4; // 滑动屏幕宽度的1/4触发切换
if (gestureState.dx > threshold && currentIndex > 0) {
// 向右滑动,显示上一个
handlePrevious();
} else if (gestureState.dx < -threshold && currentIndex < templates.length - 1) {
// 向左滑动,显示下一个
handleNext();
}
},
})
).current;
// 处理返回键Android
useEffect(() => {
const handleBackPress = () => {
if (visible) {
onClose();
return true;
}
return false;
};
const subscription = BackHandler.addEventListener('hardwareBackPress', handleBackPress);
return () => subscription.remove();
}, [visible, onClose]);
if (!visible || !currentTemplate) return null;
return (
<Modal
visible={visible}
transparent={false}
animationType="fade"
onRequestClose={handleClose}
statusBarTranslucent={true}
>
<View style={[styles.container, { backgroundColor }]}>
{/* 状态栏处理 */}
{Platform.OS === 'android' && <StatusBar hidden />}
{/* 媒体容器 */}
<TouchableOpacity
style={styles.mediaContainer}
onPress={handleMediaPress}
activeOpacity={1}
>
<View
style={styles.mediaWrapper}
{...panResponder.panHandlers}
>
{/* 根据模板类型显示对应内容 */}
{isCurrentVideo && canLoadCurrentMedia ? (
// 视频显示
<VideoPlayer
source={{ uri: currentTemplate.previewUrl }}
style={styles.video}
resizeMode={ResizeMode.COVER}
shouldPlay={true}
isLooping={true}
isMuted={false}
useNativeControls={false}
showPoster={false}
autoPlay={true}
fullscreenMode={true}
onPress={handleMediaPress}
onReady={(_status) => markMediaLoaded()}
onWebLoadedData={handleWebVideoLoad}
onError={handleMediaError}
/>
) : (
// 图片显示(包括视频无法加载时的降级显示)
<Image
source={{ uri: currentTemplate.coverImageUrl || '' }}
style={styles.image}
contentFit="contain"
onLoad={() => markMediaLoaded()}
onError={handleMediaError}
placeholder={{ blurhash: 'L6PZfSi_.AyE_3t7t7R**0o#DgR4' }}
transition={300}
/>
)}
{/* 加载指示器 */}
{isLoading && (
<View style={styles.loadingOverlay}>
<ActivityIndicator size="large" color="#4ECDC4" />
</View>
)}
{/* 左右导航指示器 */}
{currentIndex > 0 && (
<TouchableOpacity
style={styles.leftIndicator}
onPress={(e) => {
e.stopPropagation();
handlePrevious();
}}
activeOpacity={0.7}
>
<ThemedText style={styles.indicatorIcon}></ThemedText>
</TouchableOpacity>
)}
{currentIndex < templates.length - 1 && (
<TouchableOpacity
style={styles.rightIndicator}
onPress={(e) => {
e.stopPropagation();
handleNext();
}}
activeOpacity={0.7}
>
<ThemedText style={styles.indicatorIcon}></ThemedText>
</TouchableOpacity>
)}
</View>
</TouchableOpacity>
</View>
</Modal>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#000',
},
mediaContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
mediaWrapper: {
flex: 1,
width: '100%',
height: '100%',
justifyContent: 'center',
alignItems: 'center',
},
image: {
width: screenWidth,
height: screenHeight,
backgroundColor: '#000',
},
video: {
width: screenWidth,
height: screenHeight,
backgroundColor: '#000',
},
loadingOverlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.3)',
},
leftIndicator: {
position: 'absolute',
left: 20,
top: '50%',
marginTop: -20,
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'center',
alignItems: 'center',
zIndex: 10,
},
rightIndicator: {
position: 'absolute',
right: 20,
top: '50%',
marginTop: -20,
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'center',
alignItems: 'center',
zIndex: 10,
},
indicatorIcon: {
fontSize: 24,
color: '#fff',
fontWeight: 'bold',
},
});
export default FullscreenMediaModal;