Files
bw-expo-app/components/media/fullscreen-media-modal.tsx
imeepos eee280d9b3 🐛 修复视频播放器无限循环渲染问题
## 主要修复
- 修复 FullscreenMediaModal 和 FullscreenVideoModal 中的 useEffect 循环依赖
- 重构 VideoPlayer 组件的视频属性管理逻辑
- 优化 useVideoPlayer 的初始化回调机制

## 新增功能
- 新增标签 API 支持 (lib/api/tags.ts)
- 新增内容骨架屏组件 (components/profile/content-skeleton.tsx)
- 新增返回按钮组件 (components/ui/back-button.tsx)

## 改进优化
- 优化视频播放器的性能,避免重复初始化
- 修复 useEffect 依赖项导致的无限循环更新
- 完善类型定义和 API 接口

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 11:58:56 +08:00

312 lines
8.2 KiB
TypeScript
Raw 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 }}
poster={currentTemplate.coverImageUrl}
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;