diff --git a/mobile/app/file-detail.tsx b/mobile/app/file-detail.tsx index af0baad..642f4d6 100644 --- a/mobile/app/file-detail.tsx +++ b/mobile/app/file-detail.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState } from 'react'; +import React, { useRef, useState, useCallback } from 'react'; import { View, Text, @@ -23,13 +23,15 @@ const SCREEN_WIDTH = Dimensions.get('window').width; type SelectedImage = { uri: string; width: number; height: number }; +type ModalState = { images: SelectedImage[]; index: number } | null; + type RootStackParamList = { FileDetail: { fileIds: string[]; initialIndex: number }; }; type FileDetailRouteProp = RouteProp; -function DetailItem({ fileId, onSelectImage }: { fileId: string; onSelectImage?: (img: SelectedImage) => void }) { +function DetailItem({ fileId, onSelectImage }: { fileId: string; onSelectImage?: (state: ModalState) => void }) { const { data: imageData, isLoading: imageLoading } = useFileImage(fileId); const { data: fileData } = useFile(fileId); const uri = imageData?.data?.url; @@ -44,28 +46,31 @@ function DetailItem({ fileId, onSelectImage }: { fileId: string; onSelectImage?: return ( {hasPages ? ( - fullThumbnails.map((thumb) => ( - onSelectImage?.({ - uri: thumb.url, - width: SCREEN_WIDTH, - height: thumb.height * (SCREEN_WIDTH / thumb.width), - })} - > - - - )) + fullThumbnails.map((thumb, thumbIndex) => { + const allImages: SelectedImage[] = fullThumbnails.map((t) => ({ + uri: t.url, + width: SCREEN_WIDTH, + height: t.height * (SCREEN_WIDTH / t.width), + })); + return ( + onSelectImage?.({ images: allImages, index: thumbIndex })} + > + + + ); + }) ) : ( {uri ? ( onSelectImage?.({ uri, width: SCREEN_WIDTH, height: SCREEN_WIDTH })} + onPress={() => onSelectImage?.({ images: [{ uri, width: SCREEN_WIDTH, height: SCREEN_WIDTH }], index: 0 })} > @@ -128,7 +133,18 @@ export function FileDetailScreen() { const flatListRef = useRef(null); const [currentIndex, setCurrentIndex] = useState(initialIndex); - const [selectedImage, setSelectedImage] = useState(null); + const [modalState, setModalState] = useState(null); + + const handleSwipeVertical = useCallback((direction: 'up' | 'down') => { + setModalState((prev) => { + if (!prev) return prev; + const next = direction === 'up' + ? Math.min(prev.index + 1, prev.images.length - 1) + : Math.max(prev.index - 1, 0); + if (next === prev.index) return prev; + return { ...prev, index: next }; + }); + }, []); return ( @@ -153,7 +169,7 @@ export function FileDetailScreen() { }} renderItem={({ item }) => ( - + )} /> @@ -165,20 +181,29 @@ export function FileDetailScreen() { setSelectedImage(null)} + onRequestClose={() => setModalState(null)} > - {selectedImage && ( + {modalState && ( setSelectedImage(null)} + key={modalState.images[modalState.index].uri} + uri={modalState.images[modalState.index].uri} + width={modalState.images[modalState.index].width} + height={modalState.images[modalState.index].height} + onClose={() => setModalState(null)} + onSwipeVertical={handleSwipeVertical} /> + {modalState.images.length > 1 && ( + + + {modalState.index + 1} / {modalState.images.length} + + + )} )} @@ -273,4 +298,17 @@ const styles = StyleSheet.create({ color: '#fff', fontSize: 13, }, + modalPagination: { + position: 'absolute', + bottom: 40, + alignSelf: 'center', + backgroundColor: 'rgba(0,0,0,0.5)', + borderRadius: 12, + paddingHorizontal: 12, + paddingVertical: 4, + }, + modalPaginationText: { + color: '#fff', + fontSize: 13, + }, }); diff --git a/mobile/components/ZoomableImage.tsx b/mobile/components/ZoomableImage.tsx index d2c67e5..ccd8fcc 100644 --- a/mobile/components/ZoomableImage.tsx +++ b/mobile/components/ZoomableImage.tsx @@ -17,13 +17,14 @@ interface ZoomableImageProps { width: number; height: number; onClose?: () => void; + onSwipeVertical?: (direction: 'up' | 'down') => void; } const MAX_SCALE = 4; const DOUBLE_TAP_SCALE = 2.5; const SPRING_CONFIG = { damping: 20, stiffness: 200, mass: 0.5 }; -export function ZoomableImage({ uri, width, height, onClose }: ZoomableImageProps) { +export function ZoomableImage({ uri, width, height, onClose, onSwipeVertical }: ZoomableImageProps) { const scale = useSharedValue(1); const savedScale = useSharedValue(1); const translateX = useSharedValue(0); @@ -35,6 +36,10 @@ export function ZoomableImage({ uri, width, height, onClose }: ZoomableImageProp onClose?.(); }, [onClose]); + const handleSwipeVertical = useCallback((direction: 'up' | 'down') => { + onSwipeVertical?.(direction); + }, [onSwipeVertical]); + const pinch = Gesture.Pinch() .onUpdate((e) => { scale.value = Math.min(Math.max(savedScale.value * e.scale, 1), MAX_SCALE); @@ -60,8 +65,13 @@ export function ZoomableImage({ uri, width, height, onClose }: ZoomableImageProp translateY.value = savedTranslateY.value + e.translationY; } }) - .onEnd(() => { + .onEnd((e) => { if (savedScale.value <= 1) { + const absY = Math.abs(e.translationY); + const absX = Math.abs(e.translationX); + if (absY > 30 && absY > absX) { + runOnJS(handleSwipeVertical)(e.translationY > 0 ? 'down' : 'up'); + } translateX.value = withSpring(0, SPRING_CONFIG); translateY.value = withSpring(0, SPRING_CONFIG); }