motorinc-gallery-picker-pro 1.0.0

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.
@@ -0,0 +1,21 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "motorinc-gallery-picker-pro"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = package["homepage"]
10
+ s.license = package["license"]
11
+ s.authors = package["author"]
12
+
13
+ s.platforms = { :ios => "11.0" }
14
+ s.source = { :git => "https://github.com/yourrepo/gallery-picker.git", :tag => "#{s.version}" }
15
+
16
+ s.source_files = "ios/**/*.{h,m,mm,swift}"
17
+ s.requires_arc = true
18
+
19
+ s.dependency "React-Core"
20
+ s.dependency "React"
21
+ end
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "motorinc-gallery-picker-pro",
3
+ "version": "1.0.0",
4
+ "description": "A comprehensive React Native media gallery picker with smooth animations, multi-select, single-select, crop functionality, and native iOS/Android support",
5
+ "main": "index.js",
6
+ "types": "index.d.ts",
7
+ "scripts": {
8
+ "test": "echo \"Error: no test specified\" && exit 1",
9
+ "build": "echo \"No build step needed for JS files\""
10
+ },
11
+ "keywords": [
12
+ "react-native",
13
+ "gallery",
14
+ "media-picker",
15
+ "photo-picker",
16
+ "video-picker",
17
+ "image-picker",
18
+ "crop",
19
+ "multi-select",
20
+ "single-select",
21
+ "modern-ui",
22
+ "native",
23
+ "ios",
24
+ "android",
25
+ "gesture",
26
+ "animation"
27
+ ],
28
+ "author": "Shakti Ranjan Mohanty",
29
+ "license": "MIT",
30
+ "peerDependencies": {
31
+ "react": ">=18.0.0",
32
+ "react-native": ">=0.70.0",
33
+ "react-native-gesture-handler": ">=2.0.0",
34
+ "react-native-reanimated": ">=3.0.0",
35
+ "react-native-view-shot": ">=4.0.0"
36
+ },
37
+ "dependencies": {
38
+ "motorinc-global-components": "^2.1.5",
39
+ "react-native-worklets": "^0.4.1"
40
+ },
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "https://github.com/shaktimohanty21/motorinc-gallery-picker-pro.git"
44
+ },
45
+ "homepage": "https://github.com/shaktimohanty21/motorinc-gallery-picker-pro#readme",
46
+ "bugs": {
47
+ "url": "https://github.com/shaktimohanty21/motorinc-gallery-picker-pro/issues"
48
+ },
49
+ "files": [
50
+ "android/",
51
+ "ios/",
52
+ "src/",
53
+ "index.js",
54
+ "index.d.ts",
55
+ "react-native.config.js",
56
+ "motorinc-gallery-picker-pro.podspec",
57
+ "README.md",
58
+ "LICENSE"
59
+ ],
60
+ "engines": {
61
+ "node": ">=18"
62
+ }
63
+ }
@@ -0,0 +1,13 @@
1
+ module.exports = {
2
+ dependency: {
3
+ platforms: {
4
+ android: {
5
+ sourceDir: 'android',
6
+ packageImportPath: 'import com.gallerypicker.imagepicker.ImagePickerPackage;',
7
+ },
8
+ ios: {
9
+ podspecPath: 'motorinc-gallery-picker-pro.podspec',
10
+ },
11
+ },
12
+ },
13
+ };
@@ -0,0 +1,433 @@
1
+ import React, {forwardRef, useImperativeHandle} from 'react';
2
+ import {View, Text, StyleSheet, Dimensions} from 'react-native';
3
+ import {Gesture, GestureDetector} from 'react-native-gesture-handler';
4
+ import Animated, {
5
+ useSharedValue,
6
+ useAnimatedStyle,
7
+ runOnJS,
8
+ withTiming,
9
+ } from 'react-native-reanimated';
10
+ import {captureRef} from 'react-native-view-shot';
11
+ import PhotoAssetImage from './PhotoAssetImage';
12
+ import {PhotoAsset} from '../modules/ImagePickerModule';
13
+ const {width: screenWidth, height: screenHeight} = Dimensions.get('window');
14
+
15
+ export interface CropValues {
16
+ scale: number;
17
+ translateX: number;
18
+ translateY: number;
19
+ }
20
+
21
+ export interface ImageCropperRef {
22
+ captureImage: () => Promise<string>;
23
+ }
24
+
25
+ interface ImageCropperProps {
26
+ asset: PhotoAsset;
27
+ containerWidth: number;
28
+ containerHeight: number;
29
+ cropWidth: number;
30
+ cropHeight: number;
31
+ initialCropValues?: CropValues;
32
+ onCropChange?: (values: CropValues) => void;
33
+ showOverlay?: boolean;
34
+ showGrid?: boolean;
35
+ enableGestures?: boolean;
36
+ }
37
+
38
+ const ImageCropper = forwardRef<ImageCropperRef, ImageCropperProps>(
39
+ (
40
+ {
41
+ asset,
42
+ containerWidth,
43
+ containerHeight,
44
+ cropWidth,
45
+ cropHeight,
46
+ initialCropValues = {scale: 1, translateX: 0, translateY: 0},
47
+ onCropChange,
48
+ showOverlay = true,
49
+ showGrid = true,
50
+ enableGestures = true,
51
+ },
52
+ ref,
53
+ ) => {
54
+ // Shared values for gestures
55
+ const cropScale = useSharedValue(initialCropValues.scale);
56
+ const cropTranslateX = useSharedValue(initialCropValues.translateX);
57
+ const cropTranslateY = useSharedValue(initialCropValues.translateY);
58
+ const savedScale = useSharedValue(initialCropValues.scale);
59
+ const savedTranslateX = useSharedValue(initialCropValues.translateX);
60
+ const savedTranslateY = useSharedValue(initialCropValues.translateY);
61
+
62
+ // Shared value for grid opacity animation
63
+ const gridOpacity = useSharedValue(0);
64
+
65
+ // Ref for capturing the crop area
66
+ const cropAreaRef = React.useRef<View>(null);
67
+
68
+ // Expose capture method via ref
69
+ useImperativeHandle(ref, () => ({
70
+ captureImage: async () => {
71
+ if (cropAreaRef.current) {
72
+ try {
73
+ const result = await captureRef(cropAreaRef.current, {
74
+ format: 'jpg',
75
+ quality: 1,
76
+ result: 'data-uri',
77
+ });
78
+ return result;
79
+ } catch (error) {
80
+ console.error('Error capturing crop area:', error);
81
+ throw error;
82
+ }
83
+ }
84
+ throw new Error('Crop area ref not available');
85
+ },
86
+ }));
87
+
88
+ // Update values when initialCropValues change
89
+ React.useEffect(() => {
90
+ cropScale.value = initialCropValues.scale;
91
+ cropTranslateX.value = initialCropValues.translateX;
92
+ cropTranslateY.value = initialCropValues.translateY;
93
+ savedScale.value = initialCropValues.scale;
94
+ savedTranslateX.value = initialCropValues.translateX;
95
+ savedTranslateY.value = initialCropValues.translateY;
96
+ }, [initialCropValues]);
97
+
98
+ const cropAnimatedStyle = useAnimatedStyle((): any => {
99
+ return {
100
+ transform: [
101
+ {scale: cropScale.value},
102
+ {translateX: cropTranslateX.value},
103
+ {translateY: cropTranslateY.value},
104
+ ],
105
+ };
106
+ });
107
+
108
+ const gridAnimatedStyle = useAnimatedStyle(() => {
109
+ return {
110
+ opacity: gridOpacity.value,
111
+ };
112
+ });
113
+
114
+ const notifyCropChange = () => {
115
+ if (onCropChange) {
116
+ onCropChange({
117
+ scale: cropScale.value,
118
+ translateX: cropTranslateX.value,
119
+ translateY: cropTranslateY.value,
120
+ });
121
+ }
122
+ };
123
+
124
+ const gesture = Gesture.Simultaneous(
125
+ Gesture.Pan()
126
+ .onStart(() => {
127
+ savedTranslateX.value = cropTranslateX.value;
128
+ savedTranslateY.value = cropTranslateY.value;
129
+ gridOpacity.value = withTiming(1, {duration: 150});
130
+ })
131
+ .onUpdate(event => {
132
+ const newX = savedTranslateX.value + event.translationX;
133
+ const newY = savedTranslateY.value + event.translationY;
134
+
135
+ const scaledWidth = containerWidth * cropScale.value;
136
+ const scaledHeight = containerHeight * cropScale.value;
137
+
138
+ // Adjusted calculation for max translation based on crop scale
139
+ const baseMaxTranslateX = (scaledWidth - cropWidth) / 2;
140
+ const maxTranslateX = baseMaxTranslateX / cropScale.value;
141
+
142
+ if (newX > maxTranslateX) {
143
+ cropTranslateX.value = maxTranslateX;
144
+ } else if (newX < -maxTranslateX) {
145
+ cropTranslateX.value = -maxTranslateX;
146
+ } else {
147
+ cropTranslateX.value = newX;
148
+ }
149
+
150
+ // Apply same logic for Y translation with proper height
151
+ const baseMaxTranslateY = (scaledHeight - cropHeight) / 2;
152
+ const maxTranslateY = baseMaxTranslateY / cropScale.value;
153
+
154
+ if (newY > maxTranslateY) {
155
+ cropTranslateY.value = maxTranslateY;
156
+ } else if (newY < -maxTranslateY) {
157
+ cropTranslateY.value = -maxTranslateY;
158
+ } else {
159
+ cropTranslateY.value = newY;
160
+ }
161
+ })
162
+ .onEnd(() => {
163
+ 'worklet';
164
+ gridOpacity.value = withTiming(0, {duration: 300});
165
+ runOnJS(notifyCropChange)();
166
+ }),
167
+ Gesture.Pinch()
168
+ .onStart(() => {
169
+ savedScale.value = cropScale.value;
170
+ gridOpacity.value = withTiming(1, {duration: 150});
171
+ })
172
+ .onUpdate(event => {
173
+ const minScale = Math.max(
174
+ cropWidth / containerWidth,
175
+ cropHeight / containerHeight,
176
+ );
177
+ const newScale = Math.max(
178
+ minScale,
179
+ Math.min(6, savedScale.value * event.scale),
180
+ );
181
+ cropScale.value = newScale;
182
+ })
183
+ .onEnd(() => {
184
+ 'worklet';
185
+ gridOpacity.value = withTiming(0, {duration: 300});
186
+ runOnJS(notifyCropChange)();
187
+ }),
188
+ );
189
+
190
+ const imageContainer = (
191
+ <Animated.View
192
+ style={[
193
+ styles.imageContainer,
194
+ {width: containerWidth, height: containerHeight},
195
+ cropAnimatedStyle,
196
+ ]}>
197
+ <PhotoAssetImage
198
+ uri={asset.uri}
199
+ style={[styles.photoAssetImage, styles.photoResizeCover]}
200
+ width={containerWidth * 3 }
201
+ height={containerHeight * 3}
202
+ />
203
+ </Animated.View>
204
+ );
205
+
206
+ return (
207
+ <View
208
+ style={[
209
+ styles.container,
210
+ {width: containerWidth, height: containerHeight},
211
+ ]}>
212
+ {enableGestures ? (
213
+ <GestureDetector gesture={gesture}>{imageContainer}</GestureDetector>
214
+ ) : (
215
+ imageContainer
216
+ )}
217
+
218
+
219
+ {/* Hidden crop capture area - positioned to capture only the visible crop area */}
220
+ <View
221
+ ref={cropAreaRef}
222
+ style={[
223
+ styles.cropCaptureArea,
224
+ {
225
+ left: (containerWidth - cropWidth) / 2,
226
+ top: (containerHeight - cropHeight) / 2,
227
+ width: cropWidth,
228
+ height: cropHeight,
229
+ overflow: 'hidden',
230
+ },
231
+ ]}>
232
+ <Animated.View
233
+ style={[
234
+ styles.cropInnerWrapper,
235
+ {width: containerWidth, height: containerHeight},
236
+ {left: -(containerWidth - cropWidth) / 2, top: -(containerHeight - cropHeight) / 2},
237
+ cropAnimatedStyle,
238
+ ]}>
239
+ <PhotoAssetImage
240
+ uri={asset.uri}
241
+ style={[styles.photoAssetImage, styles.photoResizeCover]}
242
+ width={containerWidth * 3}
243
+ height={containerHeight * 3}
244
+ />
245
+ </Animated.View>
246
+ </View>
247
+
248
+ {/* Overlay masks - only show if enabled */}
249
+ {showOverlay && (
250
+ <View style={styles.overlayContainer} pointerEvents="none">
251
+ {/* Top mask */}
252
+ {(containerHeight - cropHeight) / 2 > 0 && (
253
+ <Animated.View
254
+ style={[
255
+ styles.mask,
256
+ {
257
+ top: 0,
258
+ left: 0,
259
+ right: 0,
260
+ height: (containerHeight - cropHeight) / 2,
261
+ },
262
+ ]}
263
+ />
264
+ )}
265
+
266
+ {/* Bottom mask */}
267
+ {(containerHeight - cropHeight) / 2 > 0 && (
268
+ <Animated.View
269
+ style={[
270
+ styles.mask,
271
+ {
272
+ bottom: 0,
273
+ left: 0,
274
+ right: 0,
275
+ height: (containerHeight - cropHeight) / 2,
276
+ },
277
+ ]}
278
+ />
279
+ )}
280
+
281
+ {/* Left mask */}
282
+ {(containerWidth - cropWidth) / 2 > 0 && (
283
+ <Animated.View
284
+ style={[
285
+ styles.mask,
286
+ {
287
+ top: (containerHeight - cropHeight) / 2,
288
+ left: 0,
289
+ width: (containerWidth - cropWidth) / 2,
290
+ height: cropHeight,
291
+ },
292
+ ]}
293
+ />
294
+ )}
295
+
296
+ {/* Right mask */}
297
+ {(containerWidth - cropWidth) / 2 > 0 && (
298
+ <Animated.View
299
+ style={[
300
+ styles.mask,
301
+ {
302
+ top: (containerHeight - cropHeight) / 2,
303
+ right: 0,
304
+ width: (containerWidth - cropWidth) / 2,
305
+ height: cropHeight,
306
+ },
307
+ ]}
308
+ />
309
+ )}
310
+
311
+ {/* Crop border and grid */}
312
+ <View
313
+ style={[
314
+ styles.cropBorder,
315
+ {left: (containerWidth - cropWidth) / 2, top: (containerHeight - cropHeight) / 2, width: cropWidth, height: cropHeight},
316
+ ]}>
317
+ {showGrid && (
318
+ <>
319
+ {/* Grid lines */}
320
+ <Animated.View
321
+ style={[
322
+ styles.gridLine,
323
+ gridAnimatedStyle,
324
+ {left: cropWidth / 3, height: cropHeight, width: 1},
325
+ ]}
326
+ />
327
+ <Animated.View
328
+ style={[
329
+ styles.gridLine,
330
+ gridAnimatedStyle,
331
+ {
332
+ left: (cropWidth * 2) / 3,
333
+ height: cropHeight,
334
+ width: 1,
335
+ },
336
+ ]}
337
+ />
338
+ <Animated.View
339
+ style={[
340
+ styles.gridLine,
341
+ gridAnimatedStyle,
342
+ {top: cropHeight / 3, width: cropWidth, height: 1},
343
+ ]}
344
+ />
345
+ <Animated.View
346
+ style={[
347
+ styles.gridLine,
348
+ gridAnimatedStyle,
349
+ {
350
+ top: (cropHeight * 2) / 3,
351
+ width: cropWidth,
352
+ height: 1,
353
+ },
354
+ ]}
355
+ />
356
+ </>
357
+ )}
358
+ </View>
359
+
360
+ </View>
361
+ )}
362
+
363
+ {/* Video indicator */}
364
+ {asset.mediaType === 'video' && (
365
+ <View style={styles.videoIndicator}>
366
+ <Text style={styles.videoIcon}>🎥</Text>
367
+ </View>
368
+ )}
369
+ </View>
370
+ );
371
+ },
372
+ );
373
+
374
+ const styles = StyleSheet.create({
375
+ container: {
376
+ backgroundColor: '#000',
377
+ overflow: 'hidden',
378
+ },
379
+ imageContainer: {
380
+ justifyContent: 'center',
381
+ alignItems: 'center',
382
+ },
383
+ photoAssetImage: {
384
+ flex: 1,
385
+ alignSelf: 'stretch',
386
+ },
387
+ photoResizeCover: {
388
+ resizeMode: 'cover' as any,
389
+ },
390
+ overlayContainer: {
391
+ position: 'absolute',
392
+ top: 0,
393
+ left: 0,
394
+ right: 0,
395
+ bottom: 0,
396
+ },
397
+ mask: {
398
+ backgroundColor: "#000",
399
+ position: 'absolute',
400
+ },
401
+
402
+ gridLine: {
403
+ position: 'absolute',
404
+ backgroundColor: 'rgba(84, 85, 86, 0.60)',
405
+ zIndex: 2,
406
+ },
407
+ cropCaptureArea: {
408
+ position: 'absolute',
409
+ backgroundColor: 'transparent',
410
+ pointerEvents: 'none',
411
+ },
412
+ cropInnerWrapper: {
413
+ justifyContent: 'center',
414
+ alignItems: 'center',
415
+ position: 'absolute',
416
+ },
417
+ cropBorder: {
418
+ position: 'absolute',
419
+ },
420
+ videoIndicator: {
421
+ position: 'absolute',
422
+ bottom: 8,
423
+ right: 8,
424
+ backgroundColor: 'rgba(0, 0, 0, 0.7)',
425
+ borderRadius: 4,
426
+ padding: 2,
427
+ },
428
+ videoIcon: {
429
+ fontSize: 10,
430
+ },
431
+ });
432
+
433
+ export default ImageCropper;