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;
}
/* ─────────────────────────────────────────────────────────────
ZoomableImage
• Gesture.Pinch() — native multi-touch pinch-to-zoom
• Gesture.Pan() — single-finger swipe-down-to-close
• Both run on the UI thread via reanimated worklets
───────────────────────────────────────────────────────────── */
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 (
);
}
/* ─────────────────────────────────────────────────────────────
FullScreenSwiper
Uses RNGH's FlatList so gesture handlers inside items
properly negotiate with the scroll gesture on Android.
───────────────────────────────────────────────────────────── */
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 (
setScrollEnabled(false)}
onScrubEnd={() => setScrollEnabled(true)}
/>
);
}
return (
setScrollEnabled(!zooming)}
/>
);
};
return (
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}
/>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#000',
},
});