| import React, { useRef, useState, useEffect } from 'react'; |
| import { View, StyleSheet, Platform, useWindowDimensions, PanResponder } from 'react-native'; |
| import { Image } from 'expo-image'; |
| import { Gesture, GestureDetector } from 'react-native-gesture-handler'; |
| import { FlatList } from 'react-native-gesture-handler'; |
| import Animated, { |
| useSharedValue, |
| useAnimatedStyle, |
| withSpring, |
| runOnJS, |
| } from 'react-native-reanimated'; |
| import CustomVideoPlayer from './CustomVideoPlayer'; |
|
|
| interface FullScreenSwiperProps { |
| assets: any[]; |
| initialIndex: number; |
| onClose: () => void; |
| onIndexChange: (index: number) => void; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| function ZoomableImage({ |
| uri, thumbUri, width, height, onSwipeDown, onZoomChange, |
| }: { |
| uri: string; thumbUri?: string; width: number; |
| onSwipeDown: () => void; onZoomChange: (zooming: boolean) => void; |
| }) { |
| const scale = useSharedValue(1); |
| const savedScale = useSharedValue(1); |
| const translateY = useSharedValue(0); |
|
|
| const pinchGesture = Gesture.Pinch() |
| .onStart(() => { |
| 'worklet'; |
| runOnJS(onZoomChange)(true); |
| }) |
| .onUpdate((e) => { |
| 'worklet'; |
| scale.value = Math.max(0.5, Math.min(5, savedScale.value * e.scale)); |
| }) |
| .onEnd(() => { |
| 'worklet'; |
| runOnJS(onZoomChange)(false); |
| if (scale.value < 1.15) { |
| scale.value = withSpring(1, { damping: 15, stiffness: 150 }); |
| savedScale.value = 1; |
| } else { |
| savedScale.value = Math.min(5, scale.value); |
| } |
| }); |
|
|
| const panGesture = Gesture.Pan() |
| .minPointers(1) |
| .maxPointers(1) |
| .activeOffsetY(20) |
| .failOffsetX([-15, 15]) |
| .onStart(() => { |
| 'worklet'; |
| runOnJS(onZoomChange)(true); |
| }) |
| .onUpdate((e) => { |
| 'worklet'; |
| translateY.value = e.translationY; |
| }) |
| .onEnd((e) => { |
| 'worklet'; |
| runOnJS(onZoomChange)(false); |
| if (e.translationY > 100) { |
| runOnJS(onSwipeDown)(); |
| } else { |
| translateY.value = withSpring(0, { damping: 15, stiffness: 150 }); |
| } |
| }); |
|
|
| const composedGesture = Gesture.Simultaneous(pinchGesture, panGesture); |
|
|
| const animatedStyle = useAnimatedStyle(() => ({ |
| transform: [ |
| { scale: scale.value }, |
| { translateY: translateY.value }, |
| ], |
| })); |
|
|
| return ( |
| <GestureDetector gesture={composedGesture}> |
| <Animated.View |
| style={[ |
| { width, height: '100%', justifyContent: 'center', alignItems: 'center' }, |
| animatedStyle, |
| ]} |
| > |
| <Image |
| source={{ uri }} |
| placeholder={thumbUri ? { uri: thumbUri } : undefined} |
| style={{ width: '100%', height: '100%' }} |
| contentFit="contain" |
| cachePolicy="memory-disk" |
| transition={200} |
| backgroundColor="transparent" |
| allowDownscaling={false} |
| /> |
| </Animated.View> |
| </GestureDetector> |
| ); |
| } |
|
|
| |
| |
| |
| |
| |
| export default function FullScreenSwiper({ |
| assets, initialIndex, onClose, onIndexChange, |
| }: FullScreenSwiperProps) { |
| const { width } = useWindowDimensions(); |
| const [activeIndex, setActiveIndex] = useState(initialIndex); |
| const [scrollEnabled, setScrollEnabled] = useState(true); |
|
|
| const onIndexChangeRef = useRef(onIndexChange); |
| useEffect(() => { onIndexChangeRef.current = onIndexChange; }, [onIndexChange]); |
|
|
| const onViewableItemsChanged = useRef(({ viewableItems }: any) => { |
| if (viewableItems.length > 0) { |
| const idx = viewableItems[0].index; |
| setActiveIndex(idx); |
| onIndexChangeRef.current(idx); |
| } |
| }).current; |
| const viewabilityConfig = useRef({ itemVisiblePercentThreshold: 50 }).current; |
|
|
| const renderItem = ({ item, index }: { item: any; index: number }) => { |
| const isVideo = |
| item.mediaType === 'video' || |
| item.filename?.toLowerCase().match(/\.(mp4|mov|avi|webm)$/i); |
| const isActive = index === activeIndex; |
|
|
| if (isVideo) { |
| return ( |
| <View style={{ width, height: '100%', justifyContent: 'center', backgroundColor: '#000' }}> |
| <CustomVideoPlayer |
| uri={item.uri} |
| shouldPlay={isActive} |
| onScrubStart={() => setScrollEnabled(false)} |
| onScrubEnd={() => setScrollEnabled(true)} |
| /> |
| </View> |
| ); |
| } |
|
|
| return ( |
| <ZoomableImage |
| uri={item.uri} |
| thumbUri={item.cloudThumbUri} |
| width={width} |
| onSwipeDown={onClose} |
| onZoomChange={(zooming) => setScrollEnabled(!zooming)} |
| /> |
| ); |
| }; |
|
|
| return ( |
| <View style={styles.container}> |
| <FlatList |
| key={`swiper-${width}`} |
| data={assets} |
| keyExtractor={(item) => item.id} |
| renderItem={renderItem} |
| horizontal |
| pagingEnabled |
| scrollEnabled={scrollEnabled} |
| showsHorizontalScrollIndicator={false} |
| initialScrollIndex={activeIndex} |
| getItemLayout={(_, index) => ({ |
| length: width, |
| offset: width * index, |
| index, |
| })} |
| onViewableItemsChanged={onViewableItemsChanged} |
| viewabilityConfig={viewabilityConfig} |
| windowSize={3} |
| maxToRenderPerBatch={3} |
| removeClippedSubviews={Platform.OS === 'android'} |
| extraData={activeIndex} |
| /> |
| </View> |
| ); |
| } |
|
|
| const styles = StyleSheet.create({ |
| container: { |
| flex: 1, |
| backgroundColor: '#000', |
| }, |
| }); |
|
|