Initial commit: expo-popcore project
This commit is contained in:
310
components/media/fullscreen-media-modal.tsx
Normal file
310
components/media/fullscreen-media-modal.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user