expo-popcore-old/components/media/fullscreen-media-modal.tsx

311 lines
8.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;