From 2dac600d067bd6dc79a1dba8b51ac8d8d2ab4a74 Mon Sep 17 00:00:00 2001 From: m Date: Thu, 16 Jul 2026 09:13:13 +0200 Subject: [PATCH] fix scrolable + zoom --- mobile/App.tsx | 15 +- mobile/app/file-detail.tsx | 41 ++-- mobile/components/ZoomableImage.tsx | 119 +++++++++++ mobile/package-lock.json | 309 +++++++++++++++++++++++++++- mobile/package.json | 5 +- 5 files changed, 462 insertions(+), 27 deletions(-) create mode 100644 mobile/components/ZoomableImage.tsx diff --git a/mobile/App.tsx b/mobile/App.tsx index 10ec5bd..b45bebc 100644 --- a/mobile/App.tsx +++ b/mobile/App.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { StatusBar } from 'expo-status-bar'; import { ActivityIndicator, View } from 'react-native'; +import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { NavigationContainer } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; @@ -70,11 +71,13 @@ function AppNavigator() { export default function App() { return ( - - - - - - + + + + + + + + ); } diff --git a/mobile/app/file-detail.tsx b/mobile/app/file-detail.tsx index 03c48e2..8c878aa 100644 --- a/mobile/app/file-detail.tsx +++ b/mobile/app/file-detail.tsx @@ -1,10 +1,9 @@ -import React, { useRef, useState } from 'react'; +import React, { useRef, useState, useCallback } from 'react'; import { View, Text, StyleSheet, FlatList, - Image, Dimensions, ActivityIndicator, ScrollView, @@ -13,6 +12,7 @@ import { RouteProp, useRoute } from '@react-navigation/native'; import { useFile, useFileImage } from '../hooks/useFiles'; import { TagChip } from '../components/TagChip'; import { FileThumbnail } from '../components/FileThumbnail'; +import { ZoomableImage } from '../components/ZoomableImage'; import type { Thumbnail } from '../types'; const SCREEN_WIDTH = Dimensions.get('window').width; @@ -23,7 +23,7 @@ type RootStackParamList = { type FileDetailRouteProp = RouteProp; -function DetailItem({ fileId }: { fileId: string }) { +function DetailItem({ fileId, onScaleChange }: { fileId: string; onScaleChange?: (scale: number) => void }) { const { data: imageData, isLoading: imageLoading } = useFileImage(fileId); const { data: fileData } = useFile(fileId); const uri = imageData?.data?.url; @@ -40,17 +40,23 @@ function DetailItem({ fileId }: { fileId: string }) { {hasPages ? ( fullThumbnails.map((thumb) => ( - )) ) : ( {uri ? ( - + ) : file ? ( (null); const [currentIndex, setCurrentIndex] = useState(initialIndex); + const [scrollEnabled, setScrollEnabled] = useState(true); + + const handleScaleChange = useCallback((scale: number) => { + setScrollEnabled(scale <= 1); + }, []); return ( @@ -118,9 +129,12 @@ export function FileDetailScreen() { data={fileIds} keyExtractor={(item) => item} horizontal - pagingEnabled + snapToInterval={SCREEN_WIDTH} + decelerationRate="fast" + disableIntervalMomentum showsHorizontalScrollIndicator={false} initialScrollIndex={initialIndex} + scrollEnabled={scrollEnabled} getItemLayout={(_, index) => ({ length: SCREEN_WIDTH, offset: SCREEN_WIDTH * index, @@ -132,7 +146,7 @@ export function FileDetailScreen() { }} renderItem={({ item }) => ( - + )} /> @@ -166,18 +180,11 @@ const styles = StyleSheet.create({ alignItems: 'center', backgroundColor: '#000', }, - image: { - width: SCREEN_WIDTH, - height: SCREEN_WIDTH, - }, pageImageContainer: { alignItems: 'center', backgroundColor: '#000', paddingVertical: 4, }, - pageImage: { - width: SCREEN_WIDTH, - }, details: { backgroundColor: '#fff', borderTopLeftRadius: 16, diff --git a/mobile/components/ZoomableImage.tsx b/mobile/components/ZoomableImage.tsx new file mode 100644 index 0000000..c82293d --- /dev/null +++ b/mobile/components/ZoomableImage.tsx @@ -0,0 +1,119 @@ +import React, { useCallback } from 'react'; +import { StyleSheet } from 'react-native'; +import Animated, { + useSharedValue, + useAnimatedStyle, + withTiming, + withSpring, + runOnJS, +} from 'react-native-reanimated'; +import { + Gesture, + GestureDetector, +} from 'react-native-gesture-handler'; + +interface ZoomableImageProps { + uri: string; + width: number; + height: number; + onScaleChange?: (scale: number) => 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, onScaleChange }: ZoomableImageProps) { + const scale = useSharedValue(1); + const savedScale = useSharedValue(1); + const translateX = useSharedValue(0); + const translateY = useSharedValue(0); + const savedTranslateX = useSharedValue(0); + const savedTranslateY = useSharedValue(0); + + const notifyScale = useCallback((s: number) => { + onScaleChange?.(s); + }, [onScaleChange]); + + const pinch = Gesture.Pinch() + .onUpdate((e) => { + scale.value = Math.min(Math.max(savedScale.value * e.scale, 1), MAX_SCALE); + runOnJS(notifyScale)(scale.value); + }) + .onEnd(() => { + if (scale.value < 1) { + scale.value = withSpring(1, SPRING_CONFIG); + translateX.value = withSpring(0, SPRING_CONFIG); + translateY.value = withSpring(0, SPRING_CONFIG); + savedScale.value = 1; + savedTranslateX.value = 0; + savedTranslateY.value = 0; + runOnJS(notifyScale)(1); + } else { + savedScale.value = scale.value; + } + }); + + const pan = Gesture.Pan() + .minDistance(10) + .activeOffsetX([-15, 15]) + .onUpdate((e) => { + if (savedScale.value > 1) { + translateX.value = savedTranslateX.value + e.translationX; + translateY.value = savedTranslateY.value + e.translationY; + } + }) + .onEnd(() => { + if (savedScale.value <= 1) { + translateX.value = withSpring(0, SPRING_CONFIG); + translateY.value = withSpring(0, SPRING_CONFIG); + } + savedTranslateX.value = translateX.value; + savedTranslateY.value = translateY.value; + }); + + const doubleTap = Gesture.Tap() + .numberOfTaps(2) + .maxDuration(250) + .onEnd(() => { + if (scale.value > 1) { + scale.value = withTiming(1, { duration: 200 }); + translateX.value = withTiming(0, { duration: 200 }); + translateY.value = withTiming(0, { duration: 200 }); + savedScale.value = 1; + savedTranslateX.value = 0; + savedTranslateY.value = 0; + runOnJS(notifyScale)(1); + } else { + scale.value = withTiming(DOUBLE_TAP_SCALE, { duration: 200 }); + savedScale.value = DOUBLE_TAP_SCALE; + runOnJS(notifyScale)(DOUBLE_TAP_SCALE); + } + }); + + const composed = Gesture.Exclusive(pinch, doubleTap, pan); + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [ + { scale: scale.value }, + { translateX: translateX.value }, + { translateY: translateY.value }, + ], + })); + + return ( + + + + ); +} + +const styles = StyleSheet.create({ + image: { + overflow: 'hidden', + }, +}); diff --git a/mobile/package-lock.json b/mobile/package-lock.json index 45cdffd..a20aca9 100644 --- a/mobile/package-lock.json +++ b/mobile/package-lock.json @@ -21,12 +21,15 @@ "expo-status-bar": "~57.0.0", "react": "19.2.3", "react-native": "0.86.0", + "react-native-gesture-handler": "~2.32.0", "react-native-mmkv": "^4.3.2", "react-native-nitro-image": "^0.15.1", "react-native-nitro-modules": "^0.36.1", + "react-native-reanimated": "4.5.0", "react-native-safe-area-context": "~5.7.0", "react-native-screens": "4.25.2", - "react-native-vision-camera": "^5.1.0" + "react-native-vision-camera": "^5.1.0", + "react-native-worklets": "0.10.0" }, "devDependencies": { "@types/react": "~19.2.2", @@ -552,6 +555,21 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz", + "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-async-generator-functions": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.7.tgz", @@ -925,6 +943,38 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-react-pure-annotations": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.29.7.tgz", @@ -941,6 +991,22 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.7.tgz", + "integrity": "sha512-rNNFV0DBAJp988xW2DOntfDoYn1eR8GGF5AT5vYc+rjyfaQkM242c9tZUHHPe7KYaiJizXPWhQTzzdbXySyhBw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-runtime": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.29.7.tgz", @@ -970,6 +1036,36 @@ "semver": "bin/semver.js" } }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.29.7.tgz", + "integrity": "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.29.7.tgz", + "integrity": "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-typescript": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz", @@ -1078,6 +1174,18 @@ "node": ">=6.9.0" } }, + "node_modules/@egjs/hammerjs": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz", + "integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==", + "license": "MIT", + "dependencies": { + "@types/hammerjs": "^2.0.36" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/@expo/code-signing-certificates": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@expo/code-signing-certificates/-/code-signing-certificates-0.0.6.tgz", @@ -1538,6 +1646,54 @@ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, + "node_modules/@react-native/babel-preset": { + "version": "0.86.0", + "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.86.0.tgz", + "integrity": "sha512-bYQcWiPySNvF4dns9Ls9gMmwgq66ohvM9Fwc/Kn8r85t66UNHxch3p1QwPiSorDelFauZwJbgo9+ReibTgvpbA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/plugin-proposal-export-default-from": "^7.24.7", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-default-from": "^7.24.7", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-transform-async-generator-functions": "^7.25.4", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.25.0", + "@babel/plugin-transform-class-properties": "^7.25.4", + "@babel/plugin-transform-classes": "^7.25.4", + "@babel/plugin-transform-destructuring": "^7.24.8", + "@babel/plugin-transform-flow-strip-types": "^7.25.2", + "@babel/plugin-transform-for-of": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-optional-catch-binding": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.8", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-react-display-name": "^7.24.7", + "@babel/plugin-transform-react-jsx": "^7.25.2", + "@babel/plugin-transform-react-jsx-self": "^7.24.7", + "@babel/plugin-transform-react-jsx-source": "^7.24.7", + "@babel/plugin-transform-regenerator": "^7.24.7", + "@babel/plugin-transform-runtime": "^7.24.7", + "@babel/plugin-transform-typescript": "^7.25.2", + "@babel/plugin-transform-unicode-regex": "^7.24.7", + "@react-native/babel-plugin-codegen": "0.86.0", + "babel-plugin-syntax-hermes-parser": "0.36.0", + "babel-plugin-transform-flow-enums": "^0.0.2", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, "node_modules/@react-native/codegen": { "version": "0.86.0", "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.86.0.tgz", @@ -1668,6 +1824,58 @@ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, + "node_modules/@react-native/metro-babel-transformer": { + "version": "0.86.0", + "resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.86.0.tgz", + "integrity": "sha512-SjKej3E5qIahqo/G+rSOrmJUQM44RyKtWtO+VfmKAAMoJWkBFomM22hTLKCIS5cdbIAJ9COAmU+KAi2wVSO0wQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/core": "^7.25.2", + "@react-native/babel-preset": "0.86.0", + "hermes-parser": "0.36.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/@react-native/metro-babel-transformer/node_modules/hermes-estree": { + "version": "0.36.0", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.36.0.tgz", + "integrity": "sha512-A1+8zn5oss2CFP7pKsOaxorQG6FNIz1WU1VDqruLPPZl3LVgeE2C5xfFg8Ow6/Ow4mSslLLtYP1J3n38eKyW9w==", + "license": "MIT", + "peer": true + }, + "node_modules/@react-native/metro-babel-transformer/node_modules/hermes-parser": { + "version": "0.36.0", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.36.0.tgz", + "integrity": "sha512-GdpwMmH5x6IpC1cijvcvYnlPB60Mh6kTSF/NFdYV/j56gYdi+0RIakYs+eqOV+bbO0SW7mgVVGSsTJxyPQfo3w==", + "license": "MIT", + "peer": true, + "dependencies": { + "hermes-estree": "0.36.0" + } + }, + "node_modules/@react-native/metro-config": { + "version": "0.86.0", + "resolved": "https://registry.npmjs.org/@react-native/metro-config/-/metro-config-0.86.0.tgz", + "integrity": "sha512-7v+xbTeEci9ZcQ/Z1OqI4RXcqN69wSMDYL5BAMvOReZ7U04+aDQ0/SQhClYPn6x2/RxM4WzMKSAuNyLKqvYVtw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@react-native/js-polyfills": "0.86.0", + "@react-native/metro-babel-transformer": "0.86.0", + "metro-config": "^0.84.3", + "metro-runtime": "^0.84.3" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, "node_modules/@react-native/normalize-colors": { "version": "0.86.0", "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.86.0.tgz", @@ -1823,6 +2031,12 @@ "react": "^18 || ^19" } }, + "node_modules/@types/hammerjs": { + "version": "2.0.46", + "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz", + "integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==", + "license": "MIT" + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -1860,12 +2074,20 @@ "version": "19.2.17", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", - "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" } }, + "node_modules/@types/react-test-renderer": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-19.1.0.tgz", + "integrity": "sha512-XD0WZrHqjNrxA/MaR9O22w/RNidWR9YZmBdRGI7wcnWGrv/3dA8wKCJ8m63Sn+tLJhcjmuhOi629N66W6kgWzQ==", + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.35", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", @@ -2580,7 +2802,6 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, "license": "MIT" }, "node_modules/debug": { @@ -3572,6 +3793,21 @@ "hermes-estree": "0.35.0" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/hosted-git-info": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", @@ -5293,6 +5529,32 @@ } } }, + "node_modules/react-native-gesture-handler": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.32.0.tgz", + "integrity": "sha512-uYIMOKlKENORq2SABE+jIjbPU+h5I/sQKcq2v16zRq848nwEp1fWRVwML4QWqijc8UcXJC25o54S8GQd4Mf2OA==", + "license": "MIT", + "dependencies": { + "@egjs/hammerjs": "^2.0.17", + "@types/react-test-renderer": "^19.1.0", + "hoist-non-react-statics": "^3.3.0", + "invariant": "^2.2.4" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-is-edge-to-edge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.3.1.tgz", + "integrity": "sha512-NIXU/iT5+ORyCc7p0z2nnlkouYKX425vuU1OEm6bMMtWWR9yvb+Xg5AZmImTKoF9abxCPqrKC3rOZsKzUYgYZA==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-mmkv": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/react-native-mmkv/-/react-native-mmkv-4.3.2.tgz", @@ -5328,6 +5590,21 @@ "react-native": "*" } }, + "node_modules/react-native-reanimated": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.5.0.tgz", + "integrity": "sha512-+iPfvK34PKKYP/p/4TaBliFkbfvjGDIvXuiiaxvISP5ip7sWegvlacwU/uAV6zNDSSmX0tDyER7PurPMKGDipA==", + "license": "MIT", + "dependencies": { + "react-native-is-edge-to-edge": "^1.3.1", + "semver": "^7.7.3" + }, + "peerDependencies": { + "react": "*", + "react-native": "0.83 - 0.86", + "react-native-worklets": "0.10.x" + } + }, "node_modules/react-native-safe-area-context": { "version": "5.7.0", "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.7.0.tgz", @@ -5364,6 +5641,32 @@ "react-native-nitro-modules": "*" } }, + "node_modules/react-native-worklets": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.10.0.tgz", + "integrity": "sha512-JhE6IxDf6iabC0qu3+TAKA4v9RlluXmoIngPQX7/QUByf75lfrsHZ6/dQhyjEWnp1EEQiwzz8Cpew140ZcewDw==", + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-class-properties": "^7.28.6", + "@babel/plugin-transform-classes": "^7.28.6", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", + "@babel/plugin-transform-optional-chaining": "^7.28.6", + "@babel/plugin-transform-shorthand-properties": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/preset-typescript": "^7.28.5", + "@babel/types": "^7.27.1", + "convert-source-map": "^2.0.0", + "semver": "^7.7.4" + }, + "peerDependencies": { + "@babel/core": "*", + "@react-native/metro-config": "*", + "react": "*", + "react-native": "0.83 - 0.86" + } + }, "node_modules/react-native/node_modules/commander": { "version": "12.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", diff --git a/mobile/package.json b/mobile/package.json index 41c37d0..40e847d 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -16,12 +16,15 @@ "expo-status-bar": "~57.0.0", "react": "19.2.3", "react-native": "0.86.0", + "react-native-gesture-handler": "~2.32.0", "react-native-mmkv": "^4.3.2", "react-native-nitro-image": "^0.15.1", "react-native-nitro-modules": "^0.36.1", + "react-native-reanimated": "4.5.0", "react-native-safe-area-context": "~5.7.0", "react-native-screens": "4.25.2", - "react-native-vision-camera": "^5.1.0" + "react-native-vision-camera": "^5.1.0", + "react-native-worklets": "0.10.0" }, "devDependencies": { "@types/react": "~19.2.2",