311 lines
8.1 KiB
TypeScript
311 lines
8.1 KiB
TypeScript
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;
|