@chem-po/react-native 0.0.24 → 0.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/components/form/UploadProgress/index.js +8 -17
- package/lib/commonjs/components/form/UploadProgress/index.js.map +1 -1
- package/lib/commonjs/components/form/input/file/index.js +26 -8
- package/lib/commonjs/components/form/input/file/index.js.map +1 -1
- package/lib/commonjs/components/image/ImageViewModal.backup.js +285 -0
- package/lib/commonjs/components/image/ImageViewModal.backup.js.map +1 -0
- package/lib/commonjs/components/image/ImageViewModal.js +79 -99
- package/lib/commonjs/components/image/ImageViewModal.js.map +1 -1
- package/lib/commonjs/components/image/ImageViewModal.old.js +285 -0
- package/lib/commonjs/components/image/ImageViewModal.old.js.map +1 -0
- package/lib/commonjs/components/loading/ProgressBar.js +78 -0
- package/lib/commonjs/components/loading/ProgressBar.js.map +1 -0
- package/lib/commonjs/components/loading/index.js +11 -0
- package/lib/commonjs/components/loading/index.js.map +1 -1
- package/lib/module/components/form/UploadProgress/index.js +6 -15
- package/lib/module/components/form/UploadProgress/index.js.map +1 -1
- package/lib/module/components/form/input/file/index.js +27 -9
- package/lib/module/components/form/input/file/index.js.map +1 -1
- package/lib/module/components/image/ImageViewModal.backup.js +277 -0
- package/lib/module/components/image/ImageViewModal.backup.js.map +1 -0
- package/lib/module/components/image/ImageViewModal.js +80 -100
- package/lib/module/components/image/ImageViewModal.js.map +1 -1
- package/lib/module/components/image/ImageViewModal.old.js +277 -0
- package/lib/module/components/image/ImageViewModal.old.js.map +1 -0
- package/lib/module/components/loading/ProgressBar.js +70 -0
- package/lib/module/components/loading/ProgressBar.js.map +1 -0
- package/lib/module/components/loading/index.js +1 -0
- package/lib/module/components/loading/index.js.map +1 -1
- package/lib/typescript/components/form/UploadProgress/index.d.ts.map +1 -1
- package/lib/typescript/components/form/input/file/index.d.ts +5 -3
- package/lib/typescript/components/form/input/file/index.d.ts.map +1 -1
- package/lib/typescript/components/image/ImageViewModal.backup.d.ts +9 -0
- package/lib/typescript/components/image/ImageViewModal.backup.d.ts.map +1 -0
- package/lib/typescript/components/image/ImageViewModal.d.ts.map +1 -1
- package/lib/typescript/components/image/ImageViewModal.old.d.ts +9 -0
- package/lib/typescript/components/image/ImageViewModal.old.d.ts.map +1 -0
- package/lib/typescript/components/loading/ProgressBar.d.ts +7 -0
- package/lib/typescript/components/loading/ProgressBar.d.ts.map +1 -0
- package/lib/typescript/components/loading/index.d.ts +1 -0
- package/lib/typescript/components/loading/index.d.ts.map +1 -1
- package/package.json +11 -10
- package/src/components/form/UploadProgress/index.tsx +3 -17
- package/src/components/form/input/file/index.tsx +28 -8
- package/src/components/image/ImageViewModal.backup.tsx +261 -0
- package/src/components/image/ImageViewModal.old.tsx +261 -0
- package/src/components/image/ImageViewModal.tsx +72 -79
- package/src/components/loading/ProgressBar.tsx +75 -0
- package/src/components/loading/index.ts +1 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { useScreen } from '@chem-po/react'
|
|
2
|
+
import { Ionicons } from '@expo/vector-icons'
|
|
3
|
+
import React, { useCallback, useMemo, useRef, useState } from 'react'
|
|
4
|
+
import { Animated, Image, Modal, StyleSheet, TouchableOpacity, View } from 'react-native'
|
|
5
|
+
import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler'
|
|
6
|
+
import { LoadingLogo } from '../loading/Loading'
|
|
7
|
+
|
|
8
|
+
interface ImageViewModalProps {
|
|
9
|
+
isOpen: boolean
|
|
10
|
+
onClose: () => void
|
|
11
|
+
src: string | null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const ImageViewModal: React.FC<ImageViewModalProps> = ({ isOpen, onClose, src }) => {
|
|
15
|
+
const [loading, setLoading] = useState(true)
|
|
16
|
+
const screenWidth = useScreen(s => s.width)
|
|
17
|
+
const screenHeight = useScreen(s => s.height)
|
|
18
|
+
const [imageSize, setImageSize] = useState({ width: screenWidth / 2, height: screenHeight / 2 })
|
|
19
|
+
|
|
20
|
+
// Animated values for zoom and pan
|
|
21
|
+
const scale = useRef(new Animated.Value(1)).current
|
|
22
|
+
const translateX = useRef(new Animated.Value(0)).current
|
|
23
|
+
const translateY = useRef(new Animated.Value(0)).current
|
|
24
|
+
|
|
25
|
+
// Gesture state
|
|
26
|
+
const savedScale = useRef(1)
|
|
27
|
+
const savedTranslateX = useRef(0)
|
|
28
|
+
const savedTranslateY = useRef(0)
|
|
29
|
+
const currentScale = useRef(1)
|
|
30
|
+
const currentTranslateX = useRef(0)
|
|
31
|
+
const currentTranslateY = useRef(0)
|
|
32
|
+
|
|
33
|
+
const { height, width } = useMemo(() => {
|
|
34
|
+
if (loading) return imageSize
|
|
35
|
+
const ratio = imageSize.width / imageSize.height
|
|
36
|
+
let h = Math.min(imageSize.height, screenHeight * 0.9)
|
|
37
|
+
let w = h * ratio
|
|
38
|
+
if (w > screenWidth * 0.9) {
|
|
39
|
+
w = Math.min(imageSize.width, screenWidth * 0.9)
|
|
40
|
+
h = w / ratio
|
|
41
|
+
}
|
|
42
|
+
return { height: h, width: w }
|
|
43
|
+
}, [screenHeight, screenWidth, imageSize, loading])
|
|
44
|
+
|
|
45
|
+
const onLoadStart = useCallback(() => setLoading(true), [])
|
|
46
|
+
const onLoad = useCallback((e: any) => {
|
|
47
|
+
const { width: naturalWidth, height: naturalHeight } = e.nativeEvent.source
|
|
48
|
+
setImageSize({ width: naturalWidth, height: naturalHeight })
|
|
49
|
+
setLoading(false)
|
|
50
|
+
}, [])
|
|
51
|
+
|
|
52
|
+
// Reset zoom and pan when modal opens/closes
|
|
53
|
+
const resetTransform = useCallback(() => {
|
|
54
|
+
scale.setValue(1)
|
|
55
|
+
translateX.setValue(0)
|
|
56
|
+
translateY.setValue(0)
|
|
57
|
+
savedScale.current = 1
|
|
58
|
+
savedTranslateX.current = 0
|
|
59
|
+
savedTranslateY.current = 0
|
|
60
|
+
currentScale.current = 1
|
|
61
|
+
currentTranslateX.current = 0
|
|
62
|
+
currentTranslateY.current = 0
|
|
63
|
+
}, [scale, translateX, translateY])
|
|
64
|
+
|
|
65
|
+
// Reset when modal closes or src changes
|
|
66
|
+
React.useEffect(() => {
|
|
67
|
+
if (!isOpen || !src) {
|
|
68
|
+
resetTransform()
|
|
69
|
+
}
|
|
70
|
+
}, [isOpen, src, resetTransform])
|
|
71
|
+
|
|
72
|
+
// Pan gesture
|
|
73
|
+
const panGesture = Gesture.Pan()
|
|
74
|
+
.onUpdate(event => {
|
|
75
|
+
// Only allow panning if zoomed in
|
|
76
|
+
if (savedScale.current > 1) {
|
|
77
|
+
const newTranslateX = savedTranslateX.current + event.translationX
|
|
78
|
+
const newTranslateY = savedTranslateY.current + event.translationY
|
|
79
|
+
|
|
80
|
+
// Calculate bounds to prevent panning too far
|
|
81
|
+
const maxTranslateX = (width * savedScale.current - width) / 2
|
|
82
|
+
const maxTranslateY = (height * savedScale.current - height) / 2
|
|
83
|
+
|
|
84
|
+
const boundedTranslateX = Math.max(-maxTranslateX, Math.min(maxTranslateX, newTranslateX))
|
|
85
|
+
const boundedTranslateY = Math.max(-maxTranslateY, Math.min(maxTranslateY, newTranslateY))
|
|
86
|
+
|
|
87
|
+
translateX.setValue(boundedTranslateX)
|
|
88
|
+
translateY.setValue(boundedTranslateY)
|
|
89
|
+
currentTranslateX.current = boundedTranslateX
|
|
90
|
+
currentTranslateY.current = boundedTranslateY
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
.onEnd(() => {
|
|
94
|
+
savedTranslateX.current = currentTranslateX.current
|
|
95
|
+
savedTranslateY.current = currentTranslateY.current
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
// Pinch gesture
|
|
99
|
+
const pinchGesture = Gesture.Pinch()
|
|
100
|
+
.onUpdate(event => {
|
|
101
|
+
const newScale = savedScale.current * event.scale
|
|
102
|
+
// Limit zoom between 1x and 5x
|
|
103
|
+
const boundedScale = Math.max(1, Math.min(5, newScale))
|
|
104
|
+
scale.setValue(boundedScale)
|
|
105
|
+
currentScale.current = boundedScale
|
|
106
|
+
|
|
107
|
+
// If zooming out to 1x, reset position
|
|
108
|
+
if (boundedScale <= 1) {
|
|
109
|
+
translateX.setValue(0)
|
|
110
|
+
translateY.setValue(0)
|
|
111
|
+
currentTranslateX.current = 0
|
|
112
|
+
currentTranslateY.current = 0
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
.onEnd(() => {
|
|
116
|
+
savedScale.current = currentScale.current
|
|
117
|
+
// If scale is close to 1, snap back to 1
|
|
118
|
+
if (savedScale.current < 1.1) {
|
|
119
|
+
Animated.parallel([
|
|
120
|
+
Animated.spring(scale, { toValue: 1, useNativeDriver: true }),
|
|
121
|
+
Animated.spring(translateX, { toValue: 0, useNativeDriver: true }),
|
|
122
|
+
Animated.spring(translateY, { toValue: 0, useNativeDriver: true }),
|
|
123
|
+
]).start()
|
|
124
|
+
savedScale.current = 1
|
|
125
|
+
savedTranslateX.current = 0
|
|
126
|
+
savedTranslateY.current = 0
|
|
127
|
+
currentScale.current = 1
|
|
128
|
+
currentTranslateX.current = 0
|
|
129
|
+
currentTranslateY.current = 0
|
|
130
|
+
} else {
|
|
131
|
+
savedTranslateX.current = currentTranslateX.current
|
|
132
|
+
savedTranslateY.current = currentTranslateY.current
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
// Double tap to zoom
|
|
137
|
+
const doubleTapGesture = Gesture.Tap()
|
|
138
|
+
.numberOfTaps(2)
|
|
139
|
+
.onEnd(() => {
|
|
140
|
+
if (savedScale.current > 1) {
|
|
141
|
+
// Zoom out
|
|
142
|
+
Animated.parallel([
|
|
143
|
+
Animated.spring(scale, { toValue: 1, useNativeDriver: true }),
|
|
144
|
+
Animated.spring(translateX, { toValue: 0, useNativeDriver: true }),
|
|
145
|
+
Animated.spring(translateY, { toValue: 0, useNativeDriver: true }),
|
|
146
|
+
]).start()
|
|
147
|
+
savedScale.current = 1
|
|
148
|
+
savedTranslateX.current = 0
|
|
149
|
+
savedTranslateY.current = 0
|
|
150
|
+
currentScale.current = 1
|
|
151
|
+
currentTranslateX.current = 0
|
|
152
|
+
currentTranslateY.current = 0
|
|
153
|
+
} else {
|
|
154
|
+
// Zoom in to 2x
|
|
155
|
+
Animated.spring(scale, { toValue: 2, useNativeDriver: true }).start()
|
|
156
|
+
savedScale.current = 2
|
|
157
|
+
currentScale.current = 2
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
// Test simple single tap gesture
|
|
162
|
+
|
|
163
|
+
// Combine gestures - simplified approach
|
|
164
|
+
const combinedGestures = Gesture.Race(
|
|
165
|
+
doubleTapGesture,
|
|
166
|
+
Gesture.Simultaneous(panGesture, pinchGesture),
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
if (!isOpen) {
|
|
170
|
+
return null
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<Modal visible={isOpen} transparent animationType="fade" onRequestClose={onClose}>
|
|
175
|
+
<GestureHandlerRootView style={styles.gestureRoot}>
|
|
176
|
+
<View style={styles.modalOverlay}>
|
|
177
|
+
<TouchableOpacity
|
|
178
|
+
style={styles.backgroundTouchable}
|
|
179
|
+
activeOpacity={1}
|
|
180
|
+
onPress={onClose}
|
|
181
|
+
/>
|
|
182
|
+
<View style={styles.contentContainer}>
|
|
183
|
+
<GestureDetector gesture={combinedGestures}>
|
|
184
|
+
<Animated.View
|
|
185
|
+
style={[
|
|
186
|
+
styles.imageContainer,
|
|
187
|
+
{
|
|
188
|
+
width,
|
|
189
|
+
height,
|
|
190
|
+
opacity: loading ? 0 : 1,
|
|
191
|
+
transform: [{ scale }, { translateX }, { translateY }],
|
|
192
|
+
},
|
|
193
|
+
]}>
|
|
194
|
+
<Image
|
|
195
|
+
source={src ? { uri: src } : undefined}
|
|
196
|
+
style={styles.image}
|
|
197
|
+
onLoadStart={onLoadStart}
|
|
198
|
+
onLoad={onLoad}
|
|
199
|
+
resizeMode="contain"
|
|
200
|
+
/>
|
|
201
|
+
</Animated.View>
|
|
202
|
+
</GestureDetector>
|
|
203
|
+
|
|
204
|
+
<TouchableOpacity style={styles.closeButton} onPress={onClose}>
|
|
205
|
+
<Ionicons name="close" size={24} color="white" />
|
|
206
|
+
</TouchableOpacity>
|
|
207
|
+
|
|
208
|
+
{loading || !src ? (
|
|
209
|
+
<View style={styles.loadingContainer}>
|
|
210
|
+
<LoadingLogo isLoading={loading} size={70} />
|
|
211
|
+
</View>
|
|
212
|
+
) : null}
|
|
213
|
+
</View>
|
|
214
|
+
</View>
|
|
215
|
+
</GestureHandlerRootView>
|
|
216
|
+
</Modal>
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const styles = StyleSheet.create({
|
|
221
|
+
gestureRoot: {
|
|
222
|
+
flex: 1,
|
|
223
|
+
},
|
|
224
|
+
modalOverlay: {
|
|
225
|
+
flex: 1,
|
|
226
|
+
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
|
227
|
+
},
|
|
228
|
+
backgroundTouchable: {
|
|
229
|
+
...StyleSheet.absoluteFillObject,
|
|
230
|
+
},
|
|
231
|
+
contentContainer: {
|
|
232
|
+
flex: 1,
|
|
233
|
+
justifyContent: 'center',
|
|
234
|
+
alignItems: 'center',
|
|
235
|
+
padding: 16,
|
|
236
|
+
},
|
|
237
|
+
imageContainer: {
|
|
238
|
+
overflow: 'hidden',
|
|
239
|
+
borderRadius: 4,
|
|
240
|
+
},
|
|
241
|
+
image: {
|
|
242
|
+
width: '100%',
|
|
243
|
+
height: '100%',
|
|
244
|
+
},
|
|
245
|
+
closeButton: {
|
|
246
|
+
position: 'absolute',
|
|
247
|
+
top: 16,
|
|
248
|
+
right: 16,
|
|
249
|
+
width: 40,
|
|
250
|
+
height: 40,
|
|
251
|
+
borderRadius: 20,
|
|
252
|
+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
253
|
+
justifyContent: 'center',
|
|
254
|
+
alignItems: 'center',
|
|
255
|
+
},
|
|
256
|
+
loadingContainer: {
|
|
257
|
+
...StyleSheet.absoluteFillObject,
|
|
258
|
+
justifyContent: 'center',
|
|
259
|
+
alignItems: 'center',
|
|
260
|
+
},
|
|
261
|
+
})
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { useScreen } from '@chem-po/react'
|
|
2
2
|
import { Ionicons } from '@expo/vector-icons'
|
|
3
|
-
import React, { useCallback,
|
|
4
|
-
import {
|
|
3
|
+
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
|
4
|
+
import { Image, Modal, StyleSheet, TouchableOpacity, View } from 'react-native'
|
|
5
5
|
import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler'
|
|
6
|
+
import Animated, { useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated'
|
|
6
7
|
import { LoadingLogo } from '../loading/Loading'
|
|
7
8
|
|
|
8
9
|
interface ImageViewModalProps {
|
|
@@ -17,18 +18,15 @@ export const ImageViewModal: React.FC<ImageViewModalProps> = ({ isOpen, onClose,
|
|
|
17
18
|
const screenHeight = useScreen(s => s.height)
|
|
18
19
|
const [imageSize, setImageSize] = useState({ width: screenWidth / 2, height: screenHeight / 2 })
|
|
19
20
|
|
|
20
|
-
//
|
|
21
|
-
const scale =
|
|
22
|
-
const translateX =
|
|
23
|
-
const translateY =
|
|
21
|
+
// Shared values for zoom and pan (reanimated)
|
|
22
|
+
const scale = useSharedValue(1)
|
|
23
|
+
const translateX = useSharedValue(0)
|
|
24
|
+
const translateY = useSharedValue(0)
|
|
24
25
|
|
|
25
|
-
//
|
|
26
|
-
const savedScale =
|
|
27
|
-
const savedTranslateX =
|
|
28
|
-
const savedTranslateY =
|
|
29
|
-
const currentScale = useRef(1)
|
|
30
|
-
const currentTranslateX = useRef(0)
|
|
31
|
-
const currentTranslateY = useRef(0)
|
|
26
|
+
// Saved state for gesture handling
|
|
27
|
+
const savedScale = useSharedValue(1)
|
|
28
|
+
const savedTranslateX = useSharedValue(0)
|
|
29
|
+
const savedTranslateY = useSharedValue(0)
|
|
32
30
|
|
|
33
31
|
const { height, width } = useMemo(() => {
|
|
34
32
|
if (loading) return imageSize
|
|
@@ -51,121 +49,116 @@ export const ImageViewModal: React.FC<ImageViewModalProps> = ({ isOpen, onClose,
|
|
|
51
49
|
|
|
52
50
|
// Reset zoom and pan when modal opens/closes
|
|
53
51
|
const resetTransform = useCallback(() => {
|
|
54
|
-
scale.
|
|
55
|
-
translateX.
|
|
56
|
-
translateY.
|
|
57
|
-
savedScale.
|
|
58
|
-
savedTranslateX.
|
|
59
|
-
savedTranslateY.
|
|
60
|
-
|
|
61
|
-
currentTranslateX.current = 0
|
|
62
|
-
currentTranslateY.current = 0
|
|
63
|
-
}, [scale, translateX, translateY])
|
|
52
|
+
scale.value = 1
|
|
53
|
+
translateX.value = 0
|
|
54
|
+
translateY.value = 0
|
|
55
|
+
savedScale.value = 1
|
|
56
|
+
savedTranslateX.value = 0
|
|
57
|
+
savedTranslateY.value = 0
|
|
58
|
+
}, [scale, translateX, translateY, savedScale, savedTranslateX, savedTranslateY])
|
|
64
59
|
|
|
65
60
|
// Reset when modal closes or src changes
|
|
66
|
-
|
|
61
|
+
useEffect(() => {
|
|
67
62
|
if (!isOpen || !src) {
|
|
68
63
|
resetTransform()
|
|
69
64
|
}
|
|
70
65
|
}, [isOpen, src, resetTransform])
|
|
71
66
|
|
|
72
|
-
// Pan gesture
|
|
67
|
+
// Pan gesture with worklet
|
|
73
68
|
const panGesture = Gesture.Pan()
|
|
74
69
|
.onUpdate(event => {
|
|
70
|
+
'worklet'
|
|
75
71
|
// Only allow panning if zoomed in
|
|
76
|
-
if (savedScale.
|
|
77
|
-
const newTranslateX = savedTranslateX.
|
|
78
|
-
const newTranslateY = savedTranslateY.
|
|
72
|
+
if (savedScale.value > 1) {
|
|
73
|
+
const newTranslateX = savedTranslateX.value + event.translationX
|
|
74
|
+
const newTranslateY = savedTranslateY.value + event.translationY
|
|
79
75
|
|
|
80
76
|
// Calculate bounds to prevent panning too far
|
|
81
|
-
const maxTranslateX = (width * savedScale.
|
|
82
|
-
const maxTranslateY = (height * savedScale.
|
|
77
|
+
const maxTranslateX = (width * savedScale.value - width) / 2
|
|
78
|
+
const maxTranslateY = (height * savedScale.value - height) / 2
|
|
83
79
|
|
|
84
80
|
const boundedTranslateX = Math.max(-maxTranslateX, Math.min(maxTranslateX, newTranslateX))
|
|
85
81
|
const boundedTranslateY = Math.max(-maxTranslateY, Math.min(maxTranslateY, newTranslateY))
|
|
86
82
|
|
|
87
|
-
translateX.
|
|
88
|
-
translateY.
|
|
89
|
-
currentTranslateX.current = boundedTranslateX
|
|
90
|
-
currentTranslateY.current = boundedTranslateY
|
|
83
|
+
translateX.value = boundedTranslateX
|
|
84
|
+
translateY.value = boundedTranslateY
|
|
91
85
|
}
|
|
92
86
|
})
|
|
93
87
|
.onEnd(() => {
|
|
94
|
-
|
|
95
|
-
|
|
88
|
+
'worklet'
|
|
89
|
+
savedTranslateX.value = translateX.value
|
|
90
|
+
savedTranslateY.value = translateY.value
|
|
96
91
|
})
|
|
97
92
|
|
|
98
|
-
// Pinch gesture
|
|
93
|
+
// Pinch gesture with worklet
|
|
99
94
|
const pinchGesture = Gesture.Pinch()
|
|
100
95
|
.onUpdate(event => {
|
|
101
|
-
|
|
96
|
+
'worklet'
|
|
97
|
+
const newScale = savedScale.value * event.scale
|
|
102
98
|
// Limit zoom between 1x and 5x
|
|
103
99
|
const boundedScale = Math.max(1, Math.min(5, newScale))
|
|
104
|
-
scale.
|
|
105
|
-
currentScale.current = boundedScale
|
|
100
|
+
scale.value = boundedScale
|
|
106
101
|
|
|
107
102
|
// If zooming out to 1x, reset position
|
|
108
103
|
if (boundedScale <= 1) {
|
|
109
|
-
translateX.
|
|
110
|
-
translateY.
|
|
111
|
-
currentTranslateX.current = 0
|
|
112
|
-
currentTranslateY.current = 0
|
|
104
|
+
translateX.value = 0
|
|
105
|
+
translateY.value = 0
|
|
113
106
|
}
|
|
114
107
|
})
|
|
115
108
|
.onEnd(() => {
|
|
116
|
-
|
|
109
|
+
'worklet'
|
|
110
|
+
savedScale.value = scale.value
|
|
117
111
|
// If scale is close to 1, snap back to 1
|
|
118
|
-
if (savedScale.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
savedTranslateX.current = 0
|
|
126
|
-
savedTranslateY.current = 0
|
|
127
|
-
currentScale.current = 1
|
|
128
|
-
currentTranslateX.current = 0
|
|
129
|
-
currentTranslateY.current = 0
|
|
112
|
+
if (savedScale.value < 1.1) {
|
|
113
|
+
scale.value = withSpring(1)
|
|
114
|
+
translateX.value = withSpring(0)
|
|
115
|
+
translateY.value = withSpring(0)
|
|
116
|
+
savedScale.value = 1
|
|
117
|
+
savedTranslateX.value = 0
|
|
118
|
+
savedTranslateY.value = 0
|
|
130
119
|
} else {
|
|
131
|
-
savedTranslateX.
|
|
132
|
-
savedTranslateY.
|
|
120
|
+
savedTranslateX.value = translateX.value
|
|
121
|
+
savedTranslateY.value = translateY.value
|
|
133
122
|
}
|
|
134
123
|
})
|
|
135
124
|
|
|
136
|
-
// Double tap to zoom
|
|
125
|
+
// Double tap to zoom with worklet
|
|
137
126
|
const doubleTapGesture = Gesture.Tap()
|
|
138
127
|
.numberOfTaps(2)
|
|
139
128
|
.onEnd(() => {
|
|
140
|
-
|
|
129
|
+
'worklet'
|
|
130
|
+
if (savedScale.value > 1) {
|
|
141
131
|
// Zoom out
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
savedTranslateX.current = 0
|
|
149
|
-
savedTranslateY.current = 0
|
|
150
|
-
currentScale.current = 1
|
|
151
|
-
currentTranslateX.current = 0
|
|
152
|
-
currentTranslateY.current = 0
|
|
132
|
+
scale.value = withSpring(1)
|
|
133
|
+
translateX.value = withSpring(0)
|
|
134
|
+
translateY.value = withSpring(0)
|
|
135
|
+
savedScale.value = 1
|
|
136
|
+
savedTranslateX.value = 0
|
|
137
|
+
savedTranslateY.value = 0
|
|
153
138
|
} else {
|
|
154
139
|
// Zoom in to 2x
|
|
155
|
-
|
|
156
|
-
savedScale.
|
|
157
|
-
currentScale.current = 2
|
|
140
|
+
scale.value = withSpring(2)
|
|
141
|
+
savedScale.value = 2
|
|
158
142
|
}
|
|
159
143
|
})
|
|
160
144
|
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
// Combine gestures - simplified approach
|
|
145
|
+
// Combine gestures
|
|
164
146
|
const combinedGestures = Gesture.Race(
|
|
165
147
|
doubleTapGesture,
|
|
166
148
|
Gesture.Simultaneous(panGesture, pinchGesture),
|
|
167
149
|
)
|
|
168
150
|
|
|
151
|
+
// Animated style using reanimated
|
|
152
|
+
const animatedStyle = useAnimatedStyle(() => {
|
|
153
|
+
return {
|
|
154
|
+
transform: [
|
|
155
|
+
{ scale: scale.value },
|
|
156
|
+
{ translateX: translateX.value },
|
|
157
|
+
{ translateY: translateY.value },
|
|
158
|
+
],
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
|
|
169
162
|
if (!isOpen) {
|
|
170
163
|
return null
|
|
171
164
|
}
|
|
@@ -188,8 +181,8 @@ export const ImageViewModal: React.FC<ImageViewModalProps> = ({ isOpen, onClose,
|
|
|
188
181
|
width,
|
|
189
182
|
height,
|
|
190
183
|
opacity: loading ? 0 : 1,
|
|
191
|
-
transform: [{ scale }, { translateX }, { translateY }],
|
|
192
184
|
},
|
|
185
|
+
animatedStyle,
|
|
193
186
|
]}>
|
|
194
187
|
<Image
|
|
195
188
|
source={src ? { uri: src } : undefined}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { useColorModeValue, useThemeValue } from '@chem-po/react'
|
|
2
|
+
import React, { useEffect, useRef } from 'react'
|
|
3
|
+
import { Animated, StyleSheet, Text, View } from 'react-native'
|
|
4
|
+
|
|
5
|
+
export const ProgressBar = ({
|
|
6
|
+
progress,
|
|
7
|
+
label,
|
|
8
|
+
height = 26,
|
|
9
|
+
}: {
|
|
10
|
+
progress: number
|
|
11
|
+
label?: string
|
|
12
|
+
height?: number
|
|
13
|
+
}) => {
|
|
14
|
+
const accentColor = useThemeValue('colors.accent.300')
|
|
15
|
+
const animatedWidth = useRef(new Animated.Value(0)).current
|
|
16
|
+
const backgroundColor = useColorModeValue('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.2)')
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
Animated.timing(animatedWidth, {
|
|
20
|
+
toValue: progress,
|
|
21
|
+
duration: 300,
|
|
22
|
+
useNativeDriver: false, // width animations require layout driver
|
|
23
|
+
}).start()
|
|
24
|
+
}, [progress, animatedWidth])
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<View style={[styles.progressContainer, { height, backgroundColor }]}>
|
|
28
|
+
<View style={styles.progressBar}>
|
|
29
|
+
<Animated.View
|
|
30
|
+
style={[
|
|
31
|
+
styles.progressFill,
|
|
32
|
+
{
|
|
33
|
+
width: animatedWidth.interpolate({
|
|
34
|
+
inputRange: [0, 1],
|
|
35
|
+
outputRange: ['0%', '100%'],
|
|
36
|
+
extrapolate: 'clamp',
|
|
37
|
+
}),
|
|
38
|
+
backgroundColor: accentColor,
|
|
39
|
+
},
|
|
40
|
+
]}
|
|
41
|
+
/>
|
|
42
|
+
</View>
|
|
43
|
+
<Text style={styles.label}>{label}</Text>
|
|
44
|
+
</View>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const styles = StyleSheet.create({
|
|
49
|
+
progressContainer: {
|
|
50
|
+
width: '100%',
|
|
51
|
+
justifyContent: 'center',
|
|
52
|
+
alignItems: 'center',
|
|
53
|
+
borderRadius: 20,
|
|
54
|
+
padding: 3,
|
|
55
|
+
},
|
|
56
|
+
progressBar: {
|
|
57
|
+
height: '100%',
|
|
58
|
+
width: '100%',
|
|
59
|
+
borderRadius: 20,
|
|
60
|
+
overflow: 'hidden',
|
|
61
|
+
},
|
|
62
|
+
progressFill: {
|
|
63
|
+
height: '100%',
|
|
64
|
+
},
|
|
65
|
+
label: {
|
|
66
|
+
position: 'absolute',
|
|
67
|
+
color: 'white',
|
|
68
|
+
fontSize: 12,
|
|
69
|
+
fontWeight: '500',
|
|
70
|
+
textTransform: 'uppercase',
|
|
71
|
+
textShadowColor: 'rgba(0, 0, 0, 0.67)',
|
|
72
|
+
textShadowOffset: { width: 1, height: 1 },
|
|
73
|
+
textShadowRadius: 3,
|
|
74
|
+
},
|
|
75
|
+
})
|