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,2639 @@
1
+ import React, {useState, useEffect, useCallback, useRef, useMemo} from 'react';
2
+ import {
3
+ View,
4
+ Text,
5
+ TouchableOpacity,
6
+ FlatList,
7
+ Modal,
8
+ SafeAreaView,
9
+ ActivityIndicator,
10
+ Alert,
11
+ AppState,
12
+ Platform,
13
+ NativeModules,
14
+ NativeEventEmitter,
15
+ RefreshControl,
16
+ Dimensions,
17
+ } from 'react-native';
18
+ import ImagePickerModule, {
19
+ PhotoAsset,
20
+ GalleryResult,
21
+ MultiSelectResult,
22
+ PermissionStatus,
23
+ SelectedImage,
24
+ MediaType,
25
+ } from '../modules/ImagePickerModule';
26
+ import {StyleSheet, ScrollView, Image} from 'react-native';
27
+ import PhotoAssetImage from './PhotoAssetImage';
28
+ import ImageCropper, {CropValues, ImageCropperRef} from './ImageCropper';
29
+ import {
30
+ Gesture,
31
+ GestureDetector,
32
+ GestureHandlerRootView,
33
+ } from 'react-native-gesture-handler';
34
+ import Animated, {
35
+ useSharedValue,
36
+ useAnimatedStyle,
37
+ withTiming,
38
+ } from 'react-native-reanimated';
39
+ import {runOnJS} from 'react-native-worklets';
40
+ import {
41
+ PlayyText,
42
+ IconComponent,
43
+ NavigationHeader,
44
+ icons,
45
+ SPACING_16,
46
+ SPACING_24,
47
+ SPACING_4,
48
+ } from 'motorinc-global-components';
49
+ import { ThemeContext, FontProvider, useTheme } from 'motorinc-global-components';
50
+ const {width: screenWidth, height: screenHeight} = Dimensions.get('window');
51
+
52
+ export interface CropParams {
53
+ x: number;
54
+ y: number;
55
+ width: number;
56
+ height: number;
57
+ scale: number;
58
+ aspectRatio: number;
59
+ }
60
+
61
+ export interface MainPhotoGalleryRef {
62
+ triggerCropCapture: () => Promise<void>;
63
+ }
64
+
65
+ interface props {
66
+ mediaType: MediaType;
67
+ renderPermissionDeniedState?: (
68
+ onPermissionRequest?: () => void,
69
+ ) => React.ReactNode;
70
+ multiSelect?: boolean;
71
+ onAssetSelected?: (asset: PhotoAsset) => void;
72
+ onSelectedAssetsChange?: (assets: PhotoAsset[]) => void;
73
+ selectedAssets?: PhotoAsset[];
74
+ hideSelectionHeader?: boolean; // Hide the selection mode header with Cancel/Next buttons
75
+ showSelectedAssetsHeader?: boolean; // Show selected assets display at top
76
+ onRemoveSelectedAsset?: (assetId: string) => void; // Callback to remove asset
77
+ onCropParamsChange?: (assetId: string, cropParams: CropParams | null) => void; // Callback for crop params
78
+ existingCropParams?: Map<string, CropParams>; // Existing crop parameters for images
79
+ onAspectRatioChange?: (aspectRatio: number) => void; // Callback for aspect ratio changes
80
+ onCroppedImagesReady?: (
81
+ croppedImages: Array<{assetId: string; dataUri: string}>,
82
+ ) => void; // Callback for cropped images
83
+ onCancel?: () => void; // Callback for cancel button
84
+ onNext?: (selectedAssets: PhotoAsset[]) => void; // Callback for next button
85
+ maxSelectionLimit?: number;
86
+ statusBarvalue?: number
87
+ }
88
+
89
+ const MainPhotoGallery = React.forwardRef<MainPhotoGalleryRef, props>(
90
+ (
91
+ {
92
+ mediaType,
93
+ renderPermissionDeniedState,
94
+ multiSelect = false,
95
+ onAssetSelected,
96
+ onSelectedAssetsChange,
97
+ selectedAssets,
98
+ hideSelectionHeader = false,
99
+ showSelectedAssetsHeader = false,
100
+ onRemoveSelectedAsset,
101
+ onCropParamsChange,
102
+ existingCropParams,
103
+ onAspectRatioChange,
104
+ onCroppedImagesReady,
105
+ onCancel,
106
+ onNext,
107
+ maxSelectionLimit = 10,
108
+ statusBarvalue
109
+ },
110
+ ref,
111
+ ) => {
112
+ const translateY = useSharedValue(0);
113
+ const startPosition = useSharedValue(0);
114
+ const [assets, setAssets] = useState<PhotoAsset[]>([]);
115
+ const [imagePickerAssets, setimagePickerAssets] = useState<PhotoAsset[]>(
116
+ [],
117
+ );
118
+ const [pickerAssetIds, setPickerAssetIds] = useState<Set<string>>(
119
+ new Set(),
120
+ );
121
+ const [scrollEnabled, setScrollEnabled] = useState(true);
122
+
123
+ // Threshold and velocity constants
124
+ const SNAP_THRESHOLD = screenWidth * 0.5; // 50% of screen width
125
+ const VELOCITY_THRESHOLD = 1000;
126
+ const [loading, setLoading] = useState(false);
127
+ const [refreshing, setRefreshing] = useState(false);
128
+ const [hasMore, setHasMore] = useState(true);
129
+ const [permissionStatus, setPermissionStatus] =
130
+ useState<PermissionStatus>('not_determined');
131
+ const [totalCount, setTotalCount] = useState(0);
132
+ const [showManageModal, setShowManageModal] = useState(false);
133
+ const [selectedImages, setSelectedImages] = useState<SelectedImage[]>([]);
134
+ const [showSelectedImagesModal, setShowSelectedImagesModal] =
135
+ useState(false);
136
+ const [selectedAssetIds, setSelectedAssetIds] = useState<Set<string>>(
137
+ new Set(),
138
+ );
139
+ const [selectedAssetOrder, setSelectedAssetOrder] = useState<string[]>([]); // Track selection order
140
+ const [isSelectionMode, setIsSelectionMode] = useState(false);
141
+ // maxSelectionLimit is used directly from props
142
+ const isUpdatingFromProp = useRef(false);
143
+ const [aspectRatio, setAspectRatio] = useState(0.8);
144
+ const [imageResizeMode, setImageResizeMode] = useState<
145
+ 'cover' | 'contain' | 'stretch' | 'repeat' | 'center' | 'none'
146
+ >('contain');
147
+
148
+ const cropScale = useSharedValue(1);
149
+ const cropTranslateX = useSharedValue(0);
150
+ const cropTranslateY = useSharedValue(0);
151
+ const savedScale = useSharedValue(1);
152
+ const savedTranslateX = useSharedValue(0);
153
+ const savedTranslateY = useSharedValue(0);
154
+
155
+ // Store refs for ImageCropper components
156
+ const imageCropperRefs = React.useRef<
157
+ Map<string, React.RefObject<ImageCropperRef | null>>
158
+ >(new Map());
159
+
160
+ // Ref for the selected assets FlatList
161
+ const selectedAssetsFlatListRef = React.useRef<FlatList>(null);
162
+ const [currentSelectedIndex, setCurrentSelectedIndex] = useState(0);
163
+
164
+ // Function to get or create ref for an asset
165
+ const getCropperRef = (assetId: string) => {
166
+ if (!imageCropperRefs.current.has(assetId)) {
167
+ imageCropperRefs.current.set(
168
+ assetId,
169
+ React.createRef<ImageCropperRef | null>(),
170
+ );
171
+ }
172
+ return imageCropperRefs.current.get(assetId)!;
173
+ };
174
+
175
+ // Functions to navigate through selected assets
176
+ const displayPreviousIndex = () => {
177
+ if (
178
+ !selectedAssets ||
179
+ selectedAssets.length === 0 ||
180
+ currentSelectedIndex === 0
181
+ )
182
+ return;
183
+
184
+ const newIndex = Math.max(0, currentSelectedIndex - 1);
185
+ setCurrentSelectedIndex(newIndex);
186
+
187
+ try {
188
+ selectedAssetsFlatListRef.current?.scrollToIndex({
189
+ index: newIndex,
190
+ animated: true,
191
+ });
192
+ } catch (error) {
193
+ console.log('ScrollToIndex error in displayPreviousIndex:', error);
194
+ }
195
+ };
196
+
197
+ const displayNextIndex = () => {
198
+ if (
199
+ !selectedAssets ||
200
+ selectedAssets.length === 0 ||
201
+ currentSelectedIndex >= selectedAssets.length - 1
202
+ )
203
+ return;
204
+
205
+ const newIndex = Math.min(
206
+ selectedAssets.length - 1,
207
+ currentSelectedIndex + 1,
208
+ );
209
+ setCurrentSelectedIndex(newIndex);
210
+
211
+ try {
212
+ selectedAssetsFlatListRef.current?.scrollToIndex({
213
+ index: newIndex,
214
+ animated: true,
215
+ });
216
+ } catch (error) {
217
+ console.log('ScrollToIndex error in displayNextIndex:', error);
218
+ }
219
+ };
220
+
221
+ // Function to capture all cropped images
222
+ const captureAllCroppedImages = async () => {
223
+ if (!selectedAssets || selectedAssets.length === 0) return;
224
+
225
+ try {
226
+ const croppedImages = [];
227
+ for (const asset of selectedAssets) {
228
+ const cropperRef = getCropperRef(asset.id);
229
+ if (cropperRef.current) {
230
+ const dataUri = await cropperRef.current.captureImage();
231
+ croppedImages.push({assetId: asset.id, dataUri});
232
+ }
233
+ }
234
+
235
+ if (onCroppedImagesReady) {
236
+ onCroppedImagesReady(croppedImages);
237
+ }
238
+ } catch (error) {
239
+ console.error('Error capturing cropped images:', error);
240
+ }
241
+ };
242
+
243
+ // Expose the capture function via ref
244
+ React.useImperativeHandle(ref, () => ({
245
+ triggerCropCapture: captureAllCroppedImages,
246
+ }));
247
+
248
+ // Reset and adjust crop values when aspect ratio changes
249
+ useEffect(() => {
250
+ // Calculate crop dimensions
251
+ let cropWidth = screenWidth;
252
+ let cropHeight = screenWidth;
253
+
254
+ if (aspectRatio === 0.8) {
255
+ cropWidth = screenWidth * 0.8 * 1.07;
256
+ cropHeight = screenWidth * 1.07;
257
+ } else if (aspectRatio === 16 / 9) {
258
+ cropWidth = screenWidth;
259
+ cropHeight = screenWidth / (16 / 9);
260
+ }
261
+
262
+ // Calculate minimum scale to fill crop area completely
263
+ // Since container now matches crop dimensions, minimum scale should be 1
264
+ const minScale = 1;
265
+
266
+ // Reset to center position with proper scale
267
+ cropTranslateX.value = withTiming(0, {duration: 300});
268
+ cropTranslateY.value = withTiming(0, {duration: 300});
269
+ cropScale.value = withTiming(minScale, {duration: 300});
270
+
271
+ // Notify parent component about aspect ratio change
272
+ if (onAspectRatioChange) {
273
+ onAspectRatioChange(aspectRatio);
274
+ }
275
+
276
+ // Reset crop parameters for all selected images
277
+ if (selectedAssets && onCropParamsChange) {
278
+ selectedAssets.forEach(asset => {
279
+ const defaultCropParams: CropParams = {
280
+ x: 0,
281
+ y: 0,
282
+ width: 0,
283
+ height: 0,
284
+ scale: 1, // Reset scale to 1 as requested
285
+ aspectRatio: aspectRatio,
286
+ };
287
+ onCropParamsChange(asset.id, defaultCropParams);
288
+ });
289
+ }
290
+ }, [aspectRatio]); // Only depend on aspectRatio, not selectedAssets
291
+
292
+ const imageSize = (screenWidth - 24) / 3 - 4;
293
+
294
+ const renderSelectedAsset = ({item}: {item: PhotoAsset}) => {
295
+ // Get crop dimensions based on aspect ratio
296
+ const getCropDimensions = () => {
297
+ if (aspectRatio === 0.8) {
298
+ return {width: screenWidth * 0.8 * 1.07, height: screenWidth * 1.07};
299
+ } else if (aspectRatio === 16 / 9) {
300
+ return {width: screenWidth, height: screenWidth / (16 / 9)};
301
+ }
302
+ return {width: screenWidth, height: screenWidth};
303
+ };
304
+
305
+ const {width: cropWidth, height: cropHeight} = getCropDimensions();
306
+
307
+ // Get existing crop params or use default
308
+ const existingCrop = existingCropParams?.get(item.id);
309
+ const initialCropValues = existingCrop
310
+ ? {
311
+ scale: existingCrop.scale,
312
+ translateX: existingCrop.x,
313
+ translateY: existingCrop.y,
314
+ }
315
+ : undefined;
316
+
317
+ const handleCropChange = (values: CropValues) => {
318
+ if (onCropParamsChange) {
319
+ const cropParams: CropParams = {
320
+ x: values.translateX,
321
+ y: values.translateY,
322
+ width: 0,
323
+ height: 0,
324
+ scale: values.scale,
325
+ aspectRatio: aspectRatio,
326
+ };
327
+ onCropParamsChange(item.id, cropParams);
328
+ }
329
+ };
330
+
331
+ return (
332
+ <View style={[styles.selectedAssetWrapper, {height: 1.07 * screenWidth, width: screenWidth}]}>
333
+ <ImageCropper
334
+ ref={getCropperRef(item.id)}
335
+ key={`${item.id}-${aspectRatio}`}
336
+ asset={item}
337
+ containerWidth={screenWidth}
338
+ containerHeight={
339
+ aspectRatio === 0.8 ? screenWidth * 1.07 : screenWidth
340
+ }
341
+ cropWidth={cropWidth}
342
+ cropHeight={cropHeight}
343
+ initialCropValues={initialCropValues}
344
+ onCropChange={handleCropChange}
345
+ enableGestures={true}
346
+ showOverlay={true}
347
+ showGrid={true}
348
+ />
349
+ </View>
350
+ );
351
+ };
352
+
353
+ // Function to notify parent component about crop parameter changes
354
+ const notifyCropParamsChange = useCallback(() => {
355
+ if (onCropParamsChange && selectedAssets && selectedAssets.length > 0) {
356
+ // Get the currently displayed asset (first one for now)
357
+ const currentAsset = selectedAssets[0];
358
+ if (currentAsset) {
359
+ const cropParams: CropParams = {
360
+ x: cropTranslateX.value,
361
+ y: cropTranslateY.value,
362
+ width: 0, // Not needed for recreation
363
+ height: 0, // Not needed for recreation
364
+ scale: cropScale.value,
365
+ aspectRatio: aspectRatio,
366
+ };
367
+ onCropParamsChange(currentAsset.id, cropParams);
368
+ }
369
+ }
370
+ }, [onCropParamsChange, selectedAssets, aspectRatio]);
371
+
372
+ // Function to extract crop parameters for the current image
373
+ const getCropParams = () => {
374
+ const getCropDimensions = () => {
375
+ if (aspectRatio === 0.8) {
376
+ return {
377
+ width: screenWidth * 0.8 * 1.07,
378
+ height: screenWidth,
379
+ };
380
+ } else if (aspectRatio === 16 / 9) {
381
+ return {
382
+ width: screenWidth,
383
+ height: screenWidth / (16 / 9),
384
+ };
385
+ } else {
386
+ return {
387
+ width: screenWidth,
388
+ height: screenWidth,
389
+ };
390
+ }
391
+ };
392
+
393
+ const {width: cropWidth, height: cropHeight} = getCropDimensions();
394
+ const cropX = (screenWidth - cropWidth) / 2;
395
+ const cropY = (screenWidth - cropHeight) / 2;
396
+
397
+ // Calculate the actual crop area on the original image
398
+ const imageWidth = screenWidth * cropScale.value;
399
+ const imageHeight = screenWidth * cropScale.value;
400
+
401
+ // Current image position
402
+ const imageX = cropTranslateX.value - (imageWidth - screenWidth) / 2;
403
+ const imageY = cropTranslateY.value - (imageHeight - screenWidth) / 2;
404
+
405
+ // Crop area relative to image
406
+ const relativeCropX = cropX - imageX;
407
+ const relativeCropY = cropY - imageY;
408
+
409
+ return {
410
+ x: relativeCropX / cropScale.value,
411
+ y: relativeCropY / cropScale.value,
412
+ width: cropWidth / cropScale.value,
413
+ height: cropHeight / cropScale.value,
414
+ scale: cropScale.value,
415
+ aspectRatio: cropWidth / cropHeight,
416
+ };
417
+ };
418
+
419
+ const renderAssetItem = ({item}: {item: PhotoAsset}) => {
420
+ const isSelected = selectedAssetIds.has(item.id);
421
+ const selectionIndex = selectedAssetOrder.indexOf(item.id) + 1;
422
+ const isVideo = item.mediaType === 'video';
423
+
424
+ return (
425
+ <TouchableOpacity
426
+ style={[styles.assetContainer, {width: imageSize, height: imageSize}]}
427
+ onPress={() => handleAssetPress(item)}
428
+ onLongPress={() => handleAssetLongPress(item)}>
429
+ <PhotoAssetImage
430
+ uri={item.uri}
431
+ style={[styles.assetImage, isSelected && styles.selectedAssetImage]}
432
+ width={imageSize}
433
+ height={imageSize}
434
+ />
435
+
436
+ {/* Video indicator */}
437
+ {isVideo && (
438
+ <View style={styles.videoIndicator}>
439
+ <Text style={styles.videoIcon}>🎥</Text>
440
+ {item.duration && (
441
+ <Text style={styles.videoDuration}>
442
+ {Math.floor(item.duration / 60)}:
443
+ {(item.duration % 60).toFixed(0).padStart(2, '0')}
444
+ </Text>
445
+ )}
446
+ </View>
447
+ )}
448
+
449
+ {/* Selection indicator - only show for multi-select mode */}
450
+ {multiSelect && (
451
+ <View style={styles.selectionIndicator}>
452
+ {isSelected ? (
453
+ <View style={styles.selectedIndicator}>
454
+ <Text style={styles.selectionNumber}>{selectionIndex}</Text>
455
+ </View>
456
+ ) : (
457
+ isSelectionMode && <View style={styles.unselectedIndicator} />
458
+ )}
459
+ </View>
460
+ )}
461
+
462
+ {/* Selection overlay */}
463
+ {isSelected && <View style={styles.selectionOverlay} />}
464
+ </TouchableOpacity>
465
+ );
466
+ };
467
+
468
+ const renderSelectedAssetsHeader = () => {
469
+ if (!showSelectedAssetsHeader) return null;
470
+
471
+ return (
472
+ <View style={styles.selectedAssetsHeaderContainer}>
473
+ {selectedAssets && selectedAssets.length > 0 ? (
474
+ <View style={styles.selectedAssetsContainer}>
475
+ <FlatList
476
+ ref={selectedAssetsFlatListRef}
477
+ data={selectedAssets}
478
+ renderItem={renderSelectedAsset}
479
+ keyExtractor={item => item.id}
480
+ horizontal
481
+ showsHorizontalScrollIndicator={false}
482
+ contentContainerStyle={styles.selectedAssetsList}
483
+ pagingEnabled
484
+ scrollEnabled={false}
485
+ getItemLayout={(data, index) => ({
486
+ length: screenWidth,
487
+ offset: screenWidth * index,
488
+ index,
489
+ })}
490
+ />
491
+ </View>
492
+ ) : (
493
+ <View style={styles.placeholderContainer}>
494
+ <View style={styles.placeholderBox} />
495
+ </View>
496
+ )}
497
+ </View>
498
+ );
499
+ };
500
+
501
+ // Helper function to get selected assets from IDs in selection order
502
+ const getSelectedAssets = useCallback(() => {
503
+ return selectedAssetOrder
504
+ .map(id => assets.find(asset => asset.id === id))
505
+ .filter(asset => asset !== undefined) as PhotoAsset[];
506
+ }, [assets, selectedAssetOrder]);
507
+
508
+ // Sync external selectedAssets prop with internal state
509
+ useEffect(() => {
510
+ if (selectedAssets && !isUpdatingFromProp.current) {
511
+ isUpdatingFromProp.current = true;
512
+ const newSelectedIds = new Set(selectedAssets.map(asset => asset.id));
513
+ const newSelectedOrder = selectedAssets.map(asset => asset.id);
514
+ setSelectedAssetIds(newSelectedIds);
515
+ setSelectedAssetOrder(newSelectedOrder);
516
+ // Don't auto-enable selection mode when syncing from external props
517
+ // Selection mode should only be enabled by long press or toggle button
518
+ // setIsSelectionMode(newSelectedIds.size > 0 && multiSelect);
519
+ // Reset flag after state updates
520
+ setTimeout(() => {
521
+ isUpdatingFromProp.current = false;
522
+ }, 0);
523
+ }
524
+ }, [selectedAssets, multiSelect]);
525
+
526
+ // Call callback when selected assets change
527
+ useEffect(() => {
528
+ if (onSelectedAssetsChange && !isUpdatingFromProp.current) {
529
+ // Add a small delay to prevent rapid updates
530
+ const timeoutId = setTimeout(() => {
531
+ if (!isUpdatingFromProp.current) {
532
+ if (selectedAssetIds.size > 0) {
533
+ const selectedAssets = getSelectedAssets();
534
+ onSelectedAssetsChange(selectedAssets);
535
+ } else {
536
+ onSelectedAssetsChange([]);
537
+ }
538
+ }
539
+ }, 50);
540
+
541
+ return () => clearTimeout(timeoutId);
542
+ }
543
+ }, [selectedAssetIds, assets, onSelectedAssetsChange, getSelectedAssets]);
544
+
545
+ const checkPermissionStatus = useCallback(async () => {
546
+ try {
547
+ const status =
548
+ await ImagePickerModule.getPhotoLibraryPermissionStatus();
549
+ console.log('Permission status from native:', status);
550
+ setPermissionStatus(status);
551
+ return status;
552
+ } catch (error) {
553
+ console.error('Error checking permission status:', error);
554
+ return 'not_determined';
555
+ }
556
+ }, []);
557
+
558
+ const requestPermission = useCallback(async () => {
559
+ try {
560
+ console.log('Requesting gallery permission...');
561
+
562
+ // For iOS, we need to use the PHPhotoLibrary authorization request
563
+ const currentStatus = await checkPermissionStatus();
564
+ console.log('Current status before request:', currentStatus);
565
+
566
+ if (currentStatus === 'not_determined') {
567
+ // This will trigger the system permission dialog
568
+ const granted = await ImagePickerModule.requestGalleryPermission();
569
+ console.log('System permission request result:', granted);
570
+
571
+ // Wait a moment and check the new status
572
+ await new Promise(resolve => setTimeout(resolve, 500));
573
+ const newStatus = await checkPermissionStatus();
574
+ console.log('Status after request:', newStatus);
575
+
576
+ return granted;
577
+ } else if (currentStatus === 'denied') {
578
+ // If already denied, show alert to go to settings
579
+ Alert.alert(
580
+ 'Permission Required',
581
+ 'Photo library access is required. Please enable it in Settings.',
582
+ [
583
+ {text: 'Cancel', style: 'cancel'},
584
+ {
585
+ text: 'Open Settings',
586
+ onPress: () =>
587
+ ImagePickerModule.openPhotoLibraryLimitedSettings(),
588
+ },
589
+ ],
590
+ );
591
+ return false;
592
+ }
593
+
594
+ return true;
595
+ } catch (error) {
596
+ console.error('Error requesting permission:', error);
597
+ Alert.alert('Error', 'Failed to request photo library permission');
598
+ return false;
599
+ }
600
+ }, [checkPermissionStatus]);
601
+
602
+ const [loadingState, setLoadingState] = useState(false);
603
+
604
+ const loadAssets = useCallback(
605
+ async (reset = false) => {
606
+ // Prevent concurrent loads
607
+ if (loadingState) {
608
+ console.log('Already loading, skipping...');
609
+ return;
610
+ }
611
+
612
+ console.log('Loading assets - reset:', reset);
613
+
614
+ try {
615
+ setLoadingState(true);
616
+
617
+ const currentOffset = reset ? 0 : assets.length;
618
+ console.log('Current offset:', currentOffset);
619
+
620
+ console.log(
621
+ '🚀 Calling fetchPhotoLibraryAssets with mediaType:',
622
+ mediaType,
623
+ );
624
+ console.log('🚀 Options:', {
625
+ limit: 20,
626
+ offset: currentOffset,
627
+ mediaType: mediaType,
628
+ });
629
+
630
+ const result: GalleryResult =
631
+ await ImagePickerModule.fetchPhotoLibraryAssets({
632
+ limit: 20,
633
+ offset: currentOffset,
634
+ mediaType: mediaType,
635
+ });
636
+
637
+ console.log(
638
+ '📱 Raw result from Android:',
639
+ JSON.stringify(result, null, 2),
640
+ );
641
+
642
+ console.log('Fetch result:', {
643
+ assetsCount: result.assets?.length || 0,
644
+ hasMore: result.hasMore,
645
+ totalCount: result.totalCount,
646
+ });
647
+
648
+ if (reset) {
649
+ setAssets(result.assets || []);
650
+ } else {
651
+ setAssets(prev => [...prev, ...(result.assets || [])]);
652
+ }
653
+
654
+ setHasMore(result.hasMore || false);
655
+ setTotalCount(result.totalCount || 0);
656
+ } catch (error) {
657
+ console.error('Error loading assets:', error);
658
+ if (reset) {
659
+ setAssets([]);
660
+ setTotalCount(0);
661
+ }
662
+ } finally {
663
+ setLoadingState(false);
664
+ setRefreshing(false);
665
+ }
666
+ },
667
+ [loadingState, mediaType],
668
+ );
669
+
670
+ const handleRefresh = useCallback(() => {
671
+ setRefreshing(true);
672
+ checkPermissionStatus().then(status => {
673
+ if (status === 'authorized' || status === 'limited') {
674
+ loadAssets(true);
675
+ } else {
676
+ setRefreshing(false);
677
+ }
678
+ });
679
+ }, [checkPermissionStatus, loadAssets]);
680
+
681
+ const handleLoadMore = useCallback(() => {
682
+ if (
683
+ hasMore &&
684
+ !loading &&
685
+ !loadingState &&
686
+ (permissionStatus === 'authorized' || permissionStatus === 'limited')
687
+ ) {
688
+ loadAssets();
689
+ }
690
+ }, [hasMore, loading, loadingState, permissionStatus, loadAssets]);
691
+
692
+ const handleGalleryIconPress = async () => {
693
+ console.log(
694
+ 'Gallery icon pressed, current permission:',
695
+ permissionStatus,
696
+ );
697
+
698
+ if (
699
+ permissionStatus === 'denied' ||
700
+ permissionStatus === 'not_determined'
701
+ ) {
702
+ const granted = await requestPermission();
703
+ console.log('Permission request result:', granted);
704
+
705
+ // Check status again after request
706
+ const newStatus = await checkPermissionStatus();
707
+ console.log('New permission status:', newStatus);
708
+
709
+ if (newStatus === 'authorized' || newStatus === 'limited') {
710
+ loadAssets(true);
711
+ }
712
+ }
713
+ };
714
+
715
+ const handleManagePress = () => {
716
+ // Check for actual limited status on both iOS and Android 14+
717
+ const isLimited = permissionStatus === 'limited';
718
+
719
+ if (isLimited) {
720
+ // For limited access, show the 3-option modal first
721
+ setShowManageModal(true);
722
+ } else {
723
+ // For full access, open regular image picker directly
724
+ handleOpenImagePicker();
725
+ }
726
+ };
727
+
728
+ const handleOpenImagePicker = async () => {
729
+ try {
730
+ console.log(
731
+ 'Opening multi-select picker for full access with mediaType:',
732
+ mediaType,
733
+ );
734
+
735
+ setLoading(true);
736
+
737
+ const result: MultiSelectResult =
738
+ await ImagePickerModule.openMultiSelectGallery({
739
+ maxSelectionLimit: maxSelectionLimit, // Use prop value
740
+ quality: 1,
741
+ maxWidth: 1024,
742
+ maxHeight: 1024,
743
+ includeBase64: true, // Include base64 data
744
+ mediaType: mediaType,
745
+ });
746
+
747
+ console.log('Multi-select picker result:', result);
748
+
749
+ if (result.success && result.images && result.images.length > 0) {
750
+ console.log(`Selected ${result.images.length} images from picker`);
751
+
752
+ // Store the selected images from picker
753
+ setSelectedImages(result.images);
754
+
755
+ // Convert SelectedImages to PhotoAssets for consistency
756
+ const convertedAssets: PhotoAsset[] = result.images.map(
757
+ (selectedImage, index) => ({
758
+ id: selectedImage.id || `picker_${Date.now()}_${index}`,
759
+ uri: selectedImage.uri,
760
+ filename: selectedImage.fileName,
761
+ width: selectedImage.width,
762
+ height: selectedImage.height,
763
+ creationDate: Date.now(),
764
+ mediaType: selectedImage.type.startsWith('video/')
765
+ ? 'video'
766
+ : 'image',
767
+ duration: selectedImage.type.startsWith('video/') ? undefined : 0,
768
+ }),
769
+ );
770
+
771
+ // Track picker asset IDs
772
+ const newPickerIds = new Set(convertedAssets.map(asset => asset.id));
773
+ setPickerAssetIds(newPickerIds);
774
+ setimagePickerAssets(convertedAssets);
775
+
776
+ // Clear previous picker assets and add new ones
777
+ setAssets(prevAssets => {
778
+ // Remove any existing picker assets using the tracked IDs
779
+ const filteredAssets = prevAssets.filter(
780
+ asset => !pickerAssetIds.has(asset.id),
781
+ );
782
+ // Add new picker assets at the beginning
783
+ return [...convertedAssets, ...filteredAssets];
784
+ });
785
+
786
+ if (multiSelect) {
787
+ // Multi-select mode: enable selection mode and select all picked images
788
+ setIsSelectionMode(true);
789
+ const newSelectedIds = new Set(
790
+ convertedAssets.map(asset => asset.id),
791
+ );
792
+ const newSelectedOrder = convertedAssets.map(asset => asset.id);
793
+ setSelectedAssetIds(newSelectedIds);
794
+ setSelectedAssetOrder(newSelectedOrder);
795
+ setCurrentSelectedIndex(0);
796
+
797
+ // Show the selected images modal for review
798
+ setShowSelectedImagesModal(true);
799
+ } else {
800
+ // Single-select mode: select only the first (and only) image
801
+ const firstAsset = convertedAssets[0];
802
+ if (firstAsset) {
803
+ setSelectedAssetIds(new Set([firstAsset.id]));
804
+ setSelectedAssetOrder([firstAsset.id]);
805
+ setCurrentSelectedIndex(0);
806
+
807
+ // Call single asset selection callback
808
+ if (onAssetSelected) {
809
+ onAssetSelected(firstAsset);
810
+ }
811
+ }
812
+ }
813
+
814
+ // Call the parent callback to notify about the selection
815
+ if (onSelectedAssetsChange) {
816
+ onSelectedAssetsChange(convertedAssets);
817
+ }
818
+
819
+ setLoading(false);
820
+ } else if (!result.success) {
821
+ console.log('User cancelled selection');
822
+ setLoading(false);
823
+ }
824
+ } catch (error) {
825
+ console.error('Error opening multi-select image picker:', error);
826
+ Alert.alert('Error', 'Failed to open image picker');
827
+ setLoading(false); // Hide loading state on error
828
+ }
829
+ };
830
+
831
+ const handleSelectImages = async () => {
832
+ setShowManageModal(false);
833
+ try {
834
+ console.log('📸 Current permission status:', permissionStatus);
835
+ console.log('📊 Total count:', totalCount);
836
+
837
+ if (Platform.OS === 'ios') {
838
+ // Check current permission status first
839
+
840
+ const currentStatus =
841
+ await ImagePickerModule.getPhotoLibraryPermissionStatus();
842
+
843
+ // Check if the method exists
844
+ if (
845
+ typeof ImagePickerModule.openPhotoLibraryLimitedPicker !==
846
+ 'function'
847
+ ) {
848
+ console.error('❌ openPhotoLibraryLimitedPicker method not found');
849
+ Alert.alert('Error', 'Photo picker method not available');
850
+ return;
851
+ }
852
+
853
+ // Try to open the limited picker regardless of reported status
854
+ // The native code will handle the availability check
855
+ console.log('🚀 Attempting to call openPhotoLibraryLimitedPicker...');
856
+ const res = await ImagePickerModule.openPhotoLibraryLimitedPicker();
857
+
858
+ console.log('✅ Limited photo picker opened successfully:', res);
859
+ } else {
860
+ // Use the universal managePhotoSelection method available on both platforms
861
+ console.log('🚀 Attempting to call managePhotoSelection...');
862
+ const res = await ImagePickerModule.managePhotoSelection();
863
+ console.log(
864
+ '✅ Photo selection management opened successfully:',
865
+ res,
866
+ );
867
+ }
868
+
869
+ // Refresh gallery after potential selection changes
870
+ setTimeout(() => {
871
+ console.log('🔄 Refreshing gallery after photo selection changes');
872
+ handleRefresh();
873
+ }, 2000);
874
+ } catch (error) {
875
+ console.error('❌ Error managing photo selection:', error);
876
+
877
+ if (error && typeof error === 'object' && 'code' in error) {
878
+ switch (error.code) {
879
+ case 'NOT_LIMITED_ACCESS':
880
+ Alert.alert(
881
+ 'Info',
882
+ 'This feature is only available when using limited photo access.',
883
+ );
884
+ break;
885
+ case 'METHOD_NOT_AVAILABLE':
886
+ Alert.alert(
887
+ 'Error',
888
+ 'Photo selection management is not available on this device or OS version',
889
+ );
890
+ break;
891
+ case 'SETTINGS_ERROR':
892
+ Alert.alert(
893
+ 'Error',
894
+ 'Unable to open photo settings. Please try again.',
895
+ );
896
+ break;
897
+ default:
898
+ Alert.alert(
899
+ 'Error',
900
+ `Failed to manage photo selection: ${
901
+ 'message' in error ? error.message : 'Unknown error'
902
+ }`,
903
+ );
904
+ }
905
+ } else {
906
+ Alert.alert(
907
+ 'Error',
908
+ `Failed to manage photo selection: ${
909
+ error instanceof Error ? error.message : 'Unknown error'
910
+ }`,
911
+ );
912
+ }
913
+ }
914
+ };
915
+
916
+ const handleChangePermissionSettings = async () => {
917
+ setShowManageModal(false);
918
+ try {
919
+ console.log('Opening app settings for permission changes...');
920
+ await ImagePickerModule.openPhotoLibraryLimitedSettings();
921
+ // Refresh after a short delay
922
+ setTimeout(() => {
923
+ handleRefresh();
924
+ }, 1000);
925
+ } catch (error) {
926
+ console.error('Error opening settings:', error);
927
+ }
928
+ };
929
+
930
+ const handleToggleLimitedAccess = async () => {
931
+ try {
932
+ const isCurrentlyLimited = permissionStatus === 'limited';
933
+ console.log(
934
+ 'Toggling limited access. Currently limited:',
935
+ isCurrentlyLimited,
936
+ );
937
+
938
+ // Toggle the mode
939
+ await ImagePickerModule.setLimitedAccessMode(!isCurrentlyLimited);
940
+
941
+ // Refresh to get new permission status and assets
942
+ setTimeout(() => {
943
+ handleRefresh();
944
+ }, 500);
945
+ } catch (error) {
946
+ console.error('Error toggling limited access:', error);
947
+ }
948
+ };
949
+
950
+ const handleSingleAssetSelection = (asset: PhotoAsset) => {
951
+ // For single selection mode - keep the selection persistent
952
+ console.log('Single asset selected:', asset);
953
+ translateY.value = withTiming(0, {
954
+ duration: 300,
955
+ });
956
+
957
+ // Set the selected asset and keep it selected
958
+ setSelectedAssetIds(new Set([asset.id]));
959
+ setSelectedAssetOrder([asset.id]);
960
+
961
+ // In single select mode, clear picker state if selecting from gallery
962
+ const isFromPicker = pickerAssetIds.has(asset.id);
963
+ if (!isFromPicker && !multiSelect) {
964
+ // Clear picker state
965
+ setSelectedImages([]);
966
+ setimagePickerAssets([]);
967
+ // Remove picker assets from main assets array
968
+ setAssets(prevAssets =>
969
+ prevAssets.filter(a => !pickerAssetIds.has(a.id)),
970
+ );
971
+ setPickerAssetIds(new Set());
972
+ }
973
+
974
+ // Call callback for single asset selection
975
+ if (onAssetSelected) {
976
+ onAssetSelected(asset);
977
+ }
978
+ };
979
+
980
+ const handleMultiAssetSelection = (asset: PhotoAsset) => {
981
+ translateY.value = withTiming(0, {
982
+ duration: 300,
983
+ });
984
+
985
+ if (!isSelectionMode) {
986
+ // Start selection mode on long press
987
+ setIsSelectionMode(true);
988
+ const newSelectedIds = new Set([asset.id]);
989
+ const newSelectedOrder = [asset.id];
990
+ setSelectedAssetIds(newSelectedIds);
991
+ setSelectedAssetOrder(newSelectedOrder);
992
+ } else {
993
+ // Toggle selection
994
+ const newSelectedIds = new Set(selectedAssetIds);
995
+ let newSelectedOrder = [...selectedAssetOrder];
996
+
997
+ if (newSelectedIds.has(asset.id)) {
998
+ // Remove from selection
999
+ const removedIndex = newSelectedOrder.indexOf(asset.id);
1000
+ newSelectedIds.delete(asset.id);
1001
+ newSelectedOrder = newSelectedOrder.filter(id => id !== asset.id);
1002
+
1003
+ // If removing a picker asset, also remove from picker state
1004
+ const isFromPicker = pickerAssetIds.has(asset.id);
1005
+ if (isFromPicker) {
1006
+ setSelectedImages(prevSelected =>
1007
+ prevSelected.filter(img => img.id !== asset.id),
1008
+ );
1009
+ setimagePickerAssets(prevAssets =>
1010
+ prevAssets.filter(pickerAsset => pickerAsset.id !== asset.id),
1011
+ );
1012
+ setPickerAssetIds(prevIds => {
1013
+ const newIds = new Set(prevIds);
1014
+ newIds.delete(asset.id);
1015
+ return newIds;
1016
+ });
1017
+ // Remove from main assets array as well
1018
+ setAssets(prevAssets => prevAssets.filter(a => a.id !== asset.id));
1019
+ }
1020
+
1021
+ // Handle current index after removal
1022
+ setTimeout(() => {
1023
+ if (newSelectedOrder.length === 0) {
1024
+ setCurrentSelectedIndex(0);
1025
+ } else {
1026
+ let newCurrentIndex = currentSelectedIndex;
1027
+
1028
+ // If removed item was before current index, shift index back
1029
+ if (removedIndex < currentSelectedIndex) {
1030
+ newCurrentIndex = currentSelectedIndex - 1;
1031
+ }
1032
+ // If removed item was the current item, stay at same position or move to last
1033
+ else if (removedIndex === currentSelectedIndex) {
1034
+ newCurrentIndex = Math.min(
1035
+ currentSelectedIndex,
1036
+ newSelectedOrder.length - 1,
1037
+ );
1038
+ }
1039
+ // If removed item was after current index, no change needed
1040
+
1041
+ // Ensure index is within bounds
1042
+ newCurrentIndex = Math.max(
1043
+ 0,
1044
+ Math.min(newCurrentIndex, newSelectedOrder.length - 1),
1045
+ );
1046
+ setCurrentSelectedIndex(newCurrentIndex);
1047
+
1048
+ // Scroll to the new current index
1049
+ if (selectedAssetsFlatListRef.current && newCurrentIndex >= 0) {
1050
+ try {
1051
+ selectedAssetsFlatListRef.current.scrollToIndex({
1052
+ index: newCurrentIndex,
1053
+ animated: false,
1054
+ });
1055
+ } catch (error) {
1056
+ console.log('ScrollToIndex error after removal:', error);
1057
+ }
1058
+ }
1059
+ }
1060
+ }, 150);
1061
+ } else if (newSelectedIds.size < maxSelectionLimit) {
1062
+ // Add to selection
1063
+ newSelectedIds.add(asset.id);
1064
+ newSelectedOrder.push(asset.id);
1065
+ const newIndex = newSelectedOrder.length - 1;
1066
+
1067
+ // Delay scroll to allow FlatList to update with new data
1068
+ setTimeout(() => {
1069
+ if (selectedAssetsFlatListRef.current && newIndex >= 0) {
1070
+ setCurrentSelectedIndex(newIndex);
1071
+
1072
+ try {
1073
+ selectedAssetsFlatListRef.current.scrollToIndex({
1074
+ index: newIndex,
1075
+ animated: false,
1076
+ });
1077
+ } catch (error) {
1078
+ console.log('ScrollToIndex error:', error);
1079
+ // Fallback to scroll to end if index is out of range
1080
+ selectedAssetsFlatListRef.current.scrollToEnd({animated: true});
1081
+ }
1082
+ }
1083
+ }, 150);
1084
+ } else {
1085
+ Alert.alert(
1086
+ 'Selection Limit',
1087
+ `You can select up to ${maxSelectionLimit} images.`,
1088
+ );
1089
+ return;
1090
+ }
1091
+
1092
+ setSelectedAssetIds(newSelectedIds);
1093
+ setSelectedAssetOrder(newSelectedOrder);
1094
+
1095
+ // Exit selection mode if no items selected
1096
+ if (newSelectedIds.size === 0) {
1097
+ setIsSelectionMode(false);
1098
+ // Clear all picker state when no selections remain
1099
+ setSelectedImages([]);
1100
+ setimagePickerAssets([]);
1101
+ // Remove all picker assets from main assets array
1102
+ setAssets(prevAssets =>
1103
+ prevAssets.filter(a => !pickerAssetIds.has(a.id)),
1104
+ );
1105
+ setPickerAssetIds(new Set());
1106
+ }
1107
+ }
1108
+ };
1109
+
1110
+ const handleAssetPress = (asset: PhotoAsset) => {
1111
+ if (isSelectionMode) {
1112
+ // In multi-select mode and selection is active
1113
+ handleMultiAssetSelection(asset);
1114
+ } else {
1115
+ // Single select mode (works for both multiSelect={true/false} when not in selection mode)
1116
+ handleSingleAssetSelection(asset);
1117
+ }
1118
+ };
1119
+
1120
+ const handleAssetLongPress = (asset: PhotoAsset) => {
1121
+ if (multiSelect) {
1122
+ // Start multi-selection mode on long press
1123
+ handleMultiAssetSelection(asset);
1124
+ translateY.value = withTiming(0, {
1125
+ duration: 300,
1126
+ });
1127
+
1128
+ }
1129
+ };
1130
+
1131
+ const toggleSelectionMode = () => {
1132
+ if (multiSelect) {
1133
+ if (isSelectionMode) {
1134
+ // If we're currently in selection mode, keep only the first selected asset
1135
+ if (selectedAssetOrder.length > 0) {
1136
+ const firstSelectedAssetId = selectedAssetOrder[0];
1137
+ const firstSelectedAsset = assets.find(
1138
+ asset => asset.id === firstSelectedAssetId,
1139
+ );
1140
+
1141
+ if (firstSelectedAsset) {
1142
+ setSelectedAssetIds(new Set([firstSelectedAssetId]));
1143
+ setSelectedAssetOrder([firstSelectedAssetId]);
1144
+
1145
+ if (onSelectedAssetsChange) {
1146
+ onSelectedAssetsChange([firstSelectedAsset]);
1147
+ }
1148
+ }
1149
+ }
1150
+ setIsSelectionMode(false);
1151
+ } else {
1152
+ // If not in selection mode, enter selection mode
1153
+ setIsSelectionMode(true);
1154
+ // Clear any existing selections when entering selection mode via toggle
1155
+ // setSelectedAssetIds(new Set());
1156
+ // setSelectedAssetOrder([]);
1157
+ }
1158
+ }
1159
+ };
1160
+
1161
+ const handleCancelSelection = () => {
1162
+ setSelectedAssetIds(new Set());
1163
+ setSelectedAssetOrder([]);
1164
+ setIsSelectionMode(false);
1165
+
1166
+ // Clear picker-related state
1167
+ setSelectedImages([]);
1168
+ setimagePickerAssets([]);
1169
+ setPickerAssetIds(new Set());
1170
+
1171
+ // Remove picker assets from main assets array using tracked IDs
1172
+ setAssets(prevAssets =>
1173
+ prevAssets.filter(asset => !pickerAssetIds.has(asset.id)),
1174
+ );
1175
+ };
1176
+
1177
+ const handleNextWithSelectedAssets = async () => {
1178
+ if (selectedAssetIds.size === 0) return;
1179
+
1180
+ try {
1181
+
1182
+ // Capture cropped images first
1183
+ await captureAllCroppedImages();
1184
+ const selectedAssets = assets.filter(asset =>
1185
+ selectedAssetIds.has(asset.id),
1186
+ );
1187
+
1188
+ // Convert PhotoAssets to SelectedImages with base64
1189
+ const processedImages: SelectedImage[] = [];
1190
+
1191
+ for (let i = 0; i < selectedAssets.length; i++) {
1192
+ const asset = selectedAssets[i];
1193
+ try {
1194
+ const isVideo = asset.mediaType === 'video';
1195
+
1196
+ let uri: string;
1197
+ let fileName: string;
1198
+ let fileType: string;
1199
+ let base64: string;
1200
+ let fileSize: number = 0;
1201
+
1202
+ if (isVideo) {
1203
+ fileName = asset.filename || `selected_video_${i + 1}.mp4`;
1204
+ fileType = 'video/mp4';
1205
+
1206
+ if (Platform.OS === 'android') {
1207
+ // For Android, use the URI directly and get base64 via native method
1208
+ uri = asset.uri;
1209
+ base64 = await ImagePickerModule.getVideoBase64(asset.uri);
1210
+ // For videos, estimate size from base64 length (rough approximation)
1211
+ fileSize = base64 ? Math.round((base64.length * 3) / 4) : 0;
1212
+ } else {
1213
+ // For iOS videos, skip base64 conversion and handle gracefully
1214
+ uri = asset.uri; // Keep the ph:// URI for display
1215
+
1216
+ console.log(
1217
+ 'iOS video - skipping base64 conversion for:',
1218
+ asset.uri,
1219
+ );
1220
+ base64 = ''; // No base64 for iOS videos
1221
+ fileSize = 0; // Unknown file size for iOS videos
1222
+ }
1223
+ } else {
1224
+ // For images, get processed image
1225
+ const assetIdentifier =
1226
+ Platform.OS === 'android' && asset.uri.startsWith('content://')
1227
+ ? asset.uri
1228
+ : asset.id;
1229
+
1230
+ uri = await ImagePickerModule.getImageForAsset(
1231
+ assetIdentifier,
1232
+ 1024, // maxWidth
1233
+ 1024, // maxHeight
1234
+ );
1235
+ fileName = asset.filename || `selected_image_${i + 1}.jpg`;
1236
+ fileType = 'image/jpeg';
1237
+
1238
+ // Convert to base64 for images
1239
+ const response = await fetch(uri);
1240
+ const blob = await response.blob();
1241
+
1242
+ const reader = new FileReader();
1243
+ const base64Promise = new Promise<string>(resolve => {
1244
+ reader.onloadend = () => {
1245
+ const base64String = reader.result as string;
1246
+ resolve(base64String);
1247
+ };
1248
+ });
1249
+ reader.readAsDataURL(blob);
1250
+ base64 = await base64Promise;
1251
+
1252
+ // Store file size from blob
1253
+ fileSize = blob.size;
1254
+ }
1255
+
1256
+ const selectedImage: SelectedImage = {
1257
+ uri: uri,
1258
+ fileName: fileName,
1259
+ fileSize: fileSize,
1260
+ width: asset.width,
1261
+ height: asset.height,
1262
+ type: fileType,
1263
+ id: asset.id,
1264
+ base64: base64,
1265
+ };
1266
+
1267
+ processedImages.push(selectedImage);
1268
+ } catch (error) {
1269
+ console.error(`Error processing asset ${asset.id}:`, error);
1270
+ }
1271
+ }
1272
+
1273
+ setSelectedImages(processedImages);
1274
+ setShowSelectedImagesModal(true);
1275
+ setIsSelectionMode(false);
1276
+ setSelectedAssetIds(new Set());
1277
+ } catch (error) {
1278
+ console.error('Error processing selected assets:', error);
1279
+ Alert.alert('Error', 'Failed to process selected media');
1280
+ } finally {
1281
+ }
1282
+ };
1283
+
1284
+ const handleAppStateChange = useCallback(
1285
+ (nextAppState: string) => {
1286
+ if (nextAppState === 'active') {
1287
+ // App came to foreground, check if permissions changed
1288
+ setTimeout(() => {
1289
+ checkPermissionStatus().then(status => {
1290
+ setPermissionStatus(currentStatus => {
1291
+ if (status !== currentStatus) {
1292
+ if (status === 'authorized' || status === 'limited') {
1293
+ loadAssets(true);
1294
+ } else {
1295
+ setAssets([]);
1296
+ setTotalCount(0);
1297
+ }
1298
+ }
1299
+ return status;
1300
+ });
1301
+ });
1302
+ }, 500);
1303
+ }
1304
+ },
1305
+ [checkPermissionStatus, loadAssets],
1306
+ );
1307
+
1308
+ useEffect(() => {
1309
+ const subscription = AppState.addEventListener(
1310
+ 'change',
1311
+ handleAppStateChange,
1312
+ );
1313
+ return () => subscription?.remove();
1314
+ }, [handleAppStateChange]);
1315
+
1316
+ useEffect(() => {
1317
+ // Listen for photo library changes from native module
1318
+ let photoLibrarySubscription: any;
1319
+
1320
+ try {
1321
+ if (NativeModules.ImagePickerModule) {
1322
+ const eventEmitter = new NativeEventEmitter(
1323
+ NativeModules.ImagePickerModule,
1324
+ );
1325
+ photoLibrarySubscription = eventEmitter.addListener(
1326
+ 'PhotoLibraryChanged',
1327
+ () => {
1328
+ console.log(
1329
+ '📸 Photo library changed event received, refreshing gallery...',
1330
+ );
1331
+ handleRefresh();
1332
+ },
1333
+ );
1334
+ }
1335
+ } catch (error) {
1336
+ console.warn('Failed to setup photo library event listener:', error);
1337
+ }
1338
+
1339
+ return () => {
1340
+ if (photoLibrarySubscription) {
1341
+ photoLibrarySubscription.remove();
1342
+ }
1343
+ };
1344
+ }, [handleRefresh]);
1345
+
1346
+ // Auto-select first asset when assets are loaded (only if no selection exists)
1347
+ useEffect(() => {
1348
+ if (
1349
+ assets.length > 0 &&
1350
+ selectedAssets?.length === 0 &&
1351
+ selectedAssetOrder.length === 0
1352
+ ) {
1353
+ const firstAsset = assets[0];
1354
+ setSelectedAssetIds(new Set([firstAsset.id]));
1355
+ setSelectedAssetOrder([firstAsset.id]);
1356
+
1357
+ if (onSelectedAssetsChange) {
1358
+ onSelectedAssetsChange([firstAsset]);
1359
+ }
1360
+ }
1361
+ }, [assets, selectedAssets, selectedAssetOrder, onSelectedAssetsChange]);
1362
+
1363
+ // Reset current index when selected assets change
1364
+ useEffect(() => {
1365
+ setCurrentSelectedIndex(0);
1366
+ }, [selectedAssets]);
1367
+
1368
+ useEffect(() => {
1369
+ const initializeGallery = async () => {
1370
+ console.log('Initializing gallery...');
1371
+ const status = await checkPermissionStatus();
1372
+ console.log('Initial permission status:', status);
1373
+
1374
+ // Add debug information for Android only
1375
+ if (Platform.OS === 'android') {
1376
+ try {
1377
+ console.log('🔍 Getting Android debug info...');
1378
+ const debugInfo =
1379
+ await ImagePickerModule.debugPermissionsAndMediaStore();
1380
+ console.log(
1381
+ '🔍 Android Debug Info:',
1382
+ JSON.stringify(debugInfo, null, 2),
1383
+ );
1384
+
1385
+ console.log('🎥 Testing video query directly...');
1386
+ const videoTest = await ImagePickerModule.testVideoQuery();
1387
+ console.log(
1388
+ '🎥 Video Test Result:',
1389
+ JSON.stringify(videoTest, null, 2),
1390
+ );
1391
+
1392
+ console.log('🎬 Running SIMPLE video test...');
1393
+ const simpleTest = await ImagePickerModule.simpleVideoTest();
1394
+ console.log(
1395
+ '🎬 Simple Test Result:',
1396
+ JSON.stringify(simpleTest, null, 2),
1397
+ );
1398
+ } catch (error) {
1399
+ console.error('🔍 Debug info error:', error);
1400
+ }
1401
+ }
1402
+
1403
+ if (status === 'authorized' || status === 'limited') {
1404
+ console.log(
1405
+ 'Loading assets for status:',
1406
+ status,
1407
+ 'with mediaType:',
1408
+ mediaType,
1409
+ );
1410
+ loadAssets(true);
1411
+ }
1412
+ };
1413
+
1414
+ initializeGallery();
1415
+ }, [mediaType]); // Reload when media type changes
1416
+
1417
+ const renderCustomHeader = () => {
1418
+ return (
1419
+ <View style={styles.customHeader}>
1420
+ <TouchableOpacity
1421
+ onPress={toggleSelectionMode}
1422
+ activeOpacity={0.8}
1423
+ hitSlop={30}>
1424
+ <View
1425
+ style={{
1426
+ position: 'absolute',
1427
+ height: 15,
1428
+ width: 15,
1429
+ borderRadius: 2,
1430
+ borderTopColor: isSelectionMode
1431
+ ? '#fff'
1432
+ : 'rgba(235, 238, 245, 0.60)',
1433
+ borderRightColor: isSelectionMode
1434
+ ? '#fff'
1435
+ : 'rgba(235, 238, 245, 0.60)',
1436
+ borderWidth: 1.5,
1437
+ bottom: 4,
1438
+ left: 4,
1439
+ }}></View>
1440
+ <View
1441
+ style={{
1442
+ position: 'absolute',
1443
+ height: 15,
1444
+ width: 15,
1445
+ borderRadius: 2,
1446
+ borderTopColor: isSelectionMode
1447
+ ? '#fff'
1448
+ : 'rgba(235, 238, 245, 0.60)',
1449
+ borderRightColor: isSelectionMode
1450
+ ? '#fff'
1451
+ : 'rgba(235, 238, 245, 0.60)',
1452
+ borderWidth: 1.5,
1453
+ bottom: 1,
1454
+ left: 1,
1455
+ }}></View>
1456
+ <View
1457
+ style={{
1458
+ width: 13,
1459
+ height: 13,
1460
+ borderRadius: 2,
1461
+ borderColor: isSelectionMode
1462
+ ? '#fff'
1463
+ : 'rgba(235, 238, 245, 0.60)',
1464
+ borderWidth: 1.5,
1465
+ }}></View>
1466
+ {/* <Text style={styles.multiSelectToggleButtonText}>
1467
+ {isSelectionMode ? '✅' : '☑️'}
1468
+ </Text> */}
1469
+ </TouchableOpacity>
1470
+
1471
+ <View style={styles.aspectRatioContainer}>
1472
+ {([0.8, 16 / 9, 1] as const).map(ratio => (
1473
+ <TouchableOpacity
1474
+ activeOpacity={0.8}
1475
+ key={ratio}
1476
+ style={[styles.aspectRatioButton]}
1477
+ onPress={() => setAspectRatio(ratio)}
1478
+ hitSlop={{top: 12, bottom: 12}}>
1479
+ <View
1480
+ style={{
1481
+ height: 20,
1482
+ aspectRatio: ratio,
1483
+ borderRadius: 2,
1484
+ borderWidth: 1.5,
1485
+ borderColor:
1486
+ aspectRatio === ratio
1487
+ ? '#fff'
1488
+ : 'rgba(235, 238, 245, 0.60)',
1489
+ justifyContent: 'center',
1490
+ alignItems: 'center',
1491
+ }}>
1492
+ <View
1493
+ style={{
1494
+ width: '100%',
1495
+ height: '100%',
1496
+ borderRadius: 2,
1497
+ borderWidth: 1.5,
1498
+ borderColor: '#000',
1499
+ backgroundColor: 'rgba(235, 238, 245, 0.60)',
1500
+ }}></View>
1501
+ </View>
1502
+ </TouchableOpacity>
1503
+ ))}
1504
+ </View>
1505
+ <View style={styles.navControls}>
1506
+ <TouchableOpacity
1507
+ activeOpacity={0.8}
1508
+ hitSlop={12}
1509
+ disabled={
1510
+ !selectedAssets ||
1511
+ selectedAssets.length === 0 ||
1512
+ currentSelectedIndex === 0
1513
+ }
1514
+ onPress={() => {
1515
+ displayPreviousIndex();
1516
+ }}>
1517
+
1518
+ <IconComponent
1519
+ name={icons.leftArrow}
1520
+ color={
1521
+ !selectedAssets ||
1522
+ selectedAssets.length === 0 ||
1523
+ currentSelectedIndex === 0
1524
+ ? 'rgba(235, 238, 245, 0.60)'
1525
+ : '#fff'
1526
+ }
1527
+ />
1528
+
1529
+ </TouchableOpacity>
1530
+
1531
+ {/* <Text
1532
+ style={{
1533
+ fontSize:16,
1534
+ color: '#fff',
1535
+ }}>
1536
+ {selectedAssets && selectedAssets.length > 0 ? `${currentSelectedIndex + 1}/${selectedAssets.length}` : '0/0'}
1537
+ </Text> */}
1538
+ <TouchableOpacity
1539
+ activeOpacity={0.8}
1540
+ hitSlop={12}
1541
+ disabled={
1542
+ !selectedAssets ||
1543
+ selectedAssets.length === 0 ||
1544
+ currentSelectedIndex >= selectedAssets.length - 1
1545
+ }
1546
+ onPress={() => {
1547
+ displayNextIndex();
1548
+ }}>
1549
+
1550
+
1551
+
1552
+ <IconComponent
1553
+ name={icons.rightArrow}
1554
+ color={
1555
+ !selectedAssets ||
1556
+ selectedAssets.length === 0 ||
1557
+ currentSelectedIndex >= selectedAssets.length - 1
1558
+ ? "rgba(235, 238, 245, 0.60)"
1559
+ : "#fff"
1560
+ }
1561
+ />
1562
+
1563
+ </TouchableOpacity>
1564
+ </View>
1565
+ </View>
1566
+ );
1567
+ };
1568
+
1569
+ const renderHeader = () => {
1570
+ // Show header when we have access to photos (authorized or limited)
1571
+ if (permissionStatus === 'authorized' || permissionStatus === 'limited') {
1572
+ // Check for actual limited status on both iOS and Android 14+
1573
+ const isLimited = permissionStatus === 'limited';
1574
+ const isAuthorize0d = !isLimited;
1575
+
1576
+ // Selection mode header - only show if not hidden
1577
+ if (isSelectionMode && !hideSelectionHeader) {
1578
+ return (
1579
+ <View style={styles.header}>
1580
+ <View style={styles.headerLeft}>
1581
+ <TouchableOpacity
1582
+ style={styles.selectionCancelButton}
1583
+ onPress={handleCancelSelection}>
1584
+ <Text style={styles.selectionCancelButtonText}>Cancel</Text>
1585
+ </TouchableOpacity>
1586
+ </View>
1587
+ <View style={styles.selectionInfo}>
1588
+ <Text style={styles.selectionText}>
1589
+ {selectedAssetIds.size} of {maxSelectionLimit}
1590
+ </Text>
1591
+ </View>
1592
+ <View style={styles.headerButtons}>
1593
+ {multiSelect && (
1594
+ <TouchableOpacity
1595
+ style={[
1596
+ styles.multiSelectToggle,
1597
+ styles.multiSelectToggleActive,
1598
+ ]}
1599
+ onPress={handleCancelSelection}>
1600
+ <Text style={styles.multiSelectToggleText}>✅</Text>
1601
+ </TouchableOpacity>
1602
+ )}
1603
+ <TouchableOpacity
1604
+ style={[
1605
+ styles.nextButton,
1606
+ selectedAssetIds.size === 0 && styles.nextButtonDisabled,
1607
+ ]}
1608
+ onPress={handleNextWithSelectedAssets}
1609
+ disabled={selectedAssetIds.size === 0}>
1610
+ <Text
1611
+ style={[
1612
+ styles.nextButtonText,
1613
+ selectedAssetIds.size === 0 &&
1614
+ styles.nextButtonTextDisabled,
1615
+ ]}>
1616
+ Next
1617
+ </Text>
1618
+ </TouchableOpacity>
1619
+ </View>
1620
+ </View>
1621
+ );
1622
+ }
1623
+
1624
+ // Normal header
1625
+ return (
1626
+ <View style={styles.header}>
1627
+ {isLimited && (
1628
+ <View style={styles.headerLeft}>
1629
+ <PlayyText FootnoteRegular style={styles.headerTitle}>
1630
+ You’ve given MotorInc access to only a select number of photos
1631
+ and videos.
1632
+ </PlayyText>
1633
+ </View>
1634
+ )}
1635
+
1636
+ <TouchableOpacity
1637
+ style={styles.manageButton}
1638
+ onPress={handleManagePress}>
1639
+ <PlayyText SubheadEmphasized style={styles.manageButtonText}>
1640
+ {isLimited ? 'Manage' : 'Albums'}
1641
+ </PlayyText>
1642
+ </TouchableOpacity>
1643
+ </View>
1644
+ );
1645
+ }
1646
+
1647
+ return null;
1648
+ };
1649
+
1650
+ const renderPermissionDeniedStateDefault = () => (
1651
+ <View style={styles.centerContainer}>
1652
+ <TouchableOpacity
1653
+ style={styles.galleryIcon}
1654
+ onPress={handleGalleryIconPress}>
1655
+ <Text style={styles.galleryIconText}>🖼️</Text>
1656
+ </TouchableOpacity>
1657
+ <Text style={styles.permissionText}>Tap to access your photos</Text>
1658
+ </View>
1659
+ );
1660
+
1661
+ const renderFooter = () => {
1662
+ if (!loading || assets.length === 0) return null;
1663
+ return (
1664
+ <View style={styles.footerLoader}>
1665
+ <ActivityIndicator size="small" color="#007AFF" />
1666
+ </View>
1667
+ );
1668
+ };
1669
+
1670
+ const renderCancelNextButtons = () => {
1671
+ // Only show buttons if we have the callbacks and are in multi-select mode
1672
+ if (!multiSelect || (!onCancel && !onNext)) {
1673
+ return null;
1674
+ }
1675
+
1676
+ const currentSelectedAssets = selectedAssets || [];
1677
+
1678
+ return (
1679
+ <NavigationHeader
1680
+ backIcon
1681
+ statusBarvalue={statusBarvalue!}
1682
+ handleBack={onCancel}
1683
+ buttonTitle='Next'
1684
+ buttontextColor='#fff'
1685
+ backIconColor='#fff'
1686
+ isButton
1687
+ label='Back'
1688
+ labelColor='#fff'
1689
+ // @ts-ignore
1690
+ handlePressButton={onNext}
1691
+ />
1692
+ )
1693
+ };
1694
+
1695
+ const renderSelectedImagesModal = () => (
1696
+ <Modal
1697
+ animationType="slide"
1698
+ transparent={false}
1699
+ visible={showSelectedImagesModal}
1700
+ onRequestClose={() => setShowSelectedImagesModal(false)}>
1701
+ <SafeAreaView style={styles.selectedImagesContainer}>
1702
+ <View style={styles.selectedImagesHeader}>
1703
+ <TouchableOpacity
1704
+ style={styles.closeButton}
1705
+ onPress={() => setShowSelectedImagesModal(false)}>
1706
+ <Text style={styles.closeButtonText}>✕</Text>
1707
+ </TouchableOpacity>
1708
+ <Text style={styles.selectedImagesTitle}>
1709
+ Selected Media ({selectedImages.length})
1710
+ </Text>
1711
+ <View style={styles.placeholder} />
1712
+ </View>
1713
+
1714
+ <ScrollView style={styles.selectedImagesScrollView}>
1715
+ {selectedImages.map((image, index) => {
1716
+ const isVideo = image.type.startsWith('video/');
1717
+ return (
1718
+ <View key={image.id || index} style={styles.selectedImageItem}>
1719
+ <View style={styles.mediaPreviewContainer}>
1720
+ <Image
1721
+ source={{uri: image.uri}}
1722
+ style={styles.selectedImage}
1723
+ resizeMode="cover"
1724
+ />
1725
+ {isVideo && (
1726
+ <View style={styles.videoOverlay}>
1727
+ <Text style={styles.videoPlayIcon}>▶️</Text>
1728
+ </View>
1729
+ )}
1730
+ </View>
1731
+ <View style={styles.imageDetails}>
1732
+ <Text style={styles.imageDetailTitle}>
1733
+ {isVideo ? `Video ${index + 1}` : `Image ${index + 1}`}
1734
+ </Text>
1735
+ <Text style={styles.imageDetailText}>
1736
+ File: {image.fileName}
1737
+ </Text>
1738
+ <Text style={styles.imageDetailText}>
1739
+ Size: {Math.round(image.fileSize / 1024)}KB
1740
+ </Text>
1741
+ <Text style={styles.imageDetailText}>
1742
+ Dimensions: {image.width} × {image.height}
1743
+ </Text>
1744
+ <Text style={styles.imageDetailText}>
1745
+ Type: {image.type}
1746
+ </Text>
1747
+ {image.base64 && (
1748
+ <Text style={styles.imageDetailText}>
1749
+ Base64: Available (
1750
+ {Math.round(image.base64.length / 1024)}KB)
1751
+ </Text>
1752
+ )}
1753
+ </View>
1754
+ </View>
1755
+ );
1756
+ })}
1757
+ </ScrollView>
1758
+ </SafeAreaView>
1759
+ </Modal>
1760
+ );
1761
+
1762
+ const renderManageModal = () => (
1763
+ <Modal
1764
+ animationType="slide"
1765
+ transparent={true}
1766
+ visible={showManageModal}
1767
+ onRequestClose={() => setShowManageModal(false)}>
1768
+ <View style={styles.modalOverlay}>
1769
+ <SafeAreaView style={styles.modalContainer}>
1770
+ <View style={styles.modalContent}>
1771
+ <Text style={styles.modalTitle}>Manage Photos</Text>
1772
+
1773
+ <TouchableOpacity
1774
+ style={styles.modalButton}
1775
+ onPress={handleSelectImages}>
1776
+ <Text style={styles.modalButtonText}>📷 Select Images</Text>
1777
+ <Text style={styles.modalButtonSubtext}>
1778
+ Update which photos this app can access
1779
+ </Text>
1780
+ </TouchableOpacity>
1781
+
1782
+ <TouchableOpacity
1783
+ style={styles.modalButton}
1784
+ onPress={handleChangePermissionSettings}>
1785
+ <Text style={styles.modalButtonText}>
1786
+ ⚙️ Change Permission Settings
1787
+ </Text>
1788
+ <Text style={styles.modalButtonSubtext}>
1789
+ Open Settings to change photo permissions
1790
+ </Text>
1791
+ </TouchableOpacity>
1792
+
1793
+ <TouchableOpacity
1794
+ style={[styles.modalButton, styles.cancelButton]}
1795
+ onPress={() => setShowManageModal(false)}>
1796
+ <Text style={[styles.modalButtonText, styles.cancelButtonText]}>
1797
+ Cancel
1798
+ </Text>
1799
+ </TouchableOpacity>
1800
+ </View>
1801
+ </SafeAreaView>
1802
+ </View>
1803
+ </Modal>
1804
+ );
1805
+
1806
+ const sections = ['selectedAssets', 'header', 'assets'];
1807
+
1808
+ const renderAssetsGrid = (): React.ReactElement => {
1809
+ if (
1810
+ permissionStatus === 'denied' ||
1811
+ permissionStatus === 'not_determined'
1812
+ ) {
1813
+ const permissionComponent = renderPermissionDeniedState
1814
+ ? renderPermissionDeniedState(handleGalleryIconPress)
1815
+ : renderPermissionDeniedStateDefault();
1816
+
1817
+ return permissionComponent as React.ReactElement;
1818
+ }
1819
+
1820
+ if (assets.length === 0) {
1821
+ return (
1822
+ <View style={styles.emptyContainer}>
1823
+ <Text style={styles.emptyText}>
1824
+ {loading ? 'Loading photos...' : 'No photos available'}
1825
+ </Text>
1826
+ </View>
1827
+ );
1828
+ }
1829
+
1830
+ return (
1831
+ <FlatList
1832
+ data={assets}
1833
+ renderItem={renderAssetItem}
1834
+ keyExtractor={item => item.id}
1835
+ numColumns={3}
1836
+ style={styles.assetsGridContainer}
1837
+ columnWrapperStyle={{
1838
+ gap: 8,
1839
+ }}
1840
+ contentContainerStyle={styles.assetsGridContent}
1841
+ showsVerticalScrollIndicator={false}
1842
+ onEndReached={handleLoadMore}
1843
+ onEndReachedThreshold={0.5}
1844
+ ListFooterComponent={renderFooter}
1845
+ // refreshControl={
1846
+ // <RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
1847
+ // }
1848
+ scrollEnabled={scrollEnabled}
1849
+ />
1850
+ );
1851
+ };
1852
+
1853
+ const gesture = Gesture.Pan()
1854
+ .onBegin(() => {})
1855
+ .onChange(() => {});
1856
+
1857
+ const selectedAssetGesture = Gesture.Pan();
1858
+
1859
+ const assetGesture = Gesture.Pan()
1860
+ .onBegin(() => {
1861
+ console.log('Asset gesture began, current position:', translateY.value);
1862
+ // Store the starting position when gesture begins
1863
+ startPosition.value = translateY.value;
1864
+ })
1865
+ .onChange(e => {
1866
+ const translation = e.translationY;
1867
+ console.log('dsfjk', e.translationY);
1868
+
1869
+ // Add translation to the starting position
1870
+ const newPosition = startPosition.value + translation;
1871
+
1872
+ // Clamp between 0 and -screenWidth
1873
+ translateY.value = Math.max(
1874
+ Math.min(newPosition, 0),
1875
+ -(screenWidth * 1.07 + 32),
1876
+ );
1877
+ })
1878
+ .onEnd(e => {
1879
+ const translation = e.translationY;
1880
+ const velocity = e.velocityY;
1881
+ const finalPosition = startPosition.value + translation;
1882
+
1883
+ let targetPosition = 0;
1884
+
1885
+ // Determine target based on velocity first
1886
+ if (Math.abs(velocity) > VELOCITY_THRESHOLD) {
1887
+ // Fast swipe - use velocity direction
1888
+ targetPosition = velocity < 0 ? -(screenWidth * 1.07 + 44) : 0;
1889
+
1890
+ console.log(
1891
+ 'Asset velocity snap:',
1892
+ velocity < 0 ? 'to -screenWidth' : 'to 0',
1893
+ );
1894
+ } else {
1895
+ // Slow swipe - use position threshold
1896
+ const distanceFromTop = Math.abs(finalPosition);
1897
+ const distanceFromBottom = Math.abs(
1898
+ finalPosition + (screenWidth * 1.07 + 44),
1899
+ );
1900
+
1901
+ if (distanceFromTop < distanceFromBottom) {
1902
+ targetPosition = 0;
1903
+ console.log('Asset position snap: to 0 (closer to top)');
1904
+ } else {
1905
+ targetPosition = -(screenWidth * 1.07 + 44);
1906
+ console.log(
1907
+ 'Asset position snap: to -screenWidth (closer to bottom)',
1908
+ );
1909
+ }
1910
+ }
1911
+
1912
+ // Animate to target position with smooth timing
1913
+ translateY.value = withTiming(targetPosition, {
1914
+ duration: 300,
1915
+ });
1916
+ });
1917
+
1918
+ const renderHeaderGesture = Gesture.Pan()
1919
+ .onBegin(() => {
1920
+ console.log(
1921
+ 'Header gesture began, current position:',
1922
+ translateY.value,
1923
+ );
1924
+ // Store the starting position when gesture begins
1925
+ startPosition.value = translateY.value;
1926
+ })
1927
+ .onChange(e => {
1928
+ const translation = e.translationY;
1929
+
1930
+ // Add translation to the starting position
1931
+ const newPosition = startPosition.value + translation;
1932
+
1933
+ // Clamp between 0 and -screenWidth
1934
+ translateY.value = Math.max(
1935
+ Math.min(newPosition, 0),
1936
+ -(screenWidth * 1.07 + 44),
1937
+ );
1938
+ })
1939
+ .onEnd(e => {
1940
+ const translation = e.translationY;
1941
+ const velocity = e.velocityY;
1942
+ const finalPosition = startPosition.value + translation;
1943
+
1944
+ let targetPosition = 0;
1945
+
1946
+ // Determine target based on velocity first
1947
+ if (Math.abs(velocity) > VELOCITY_THRESHOLD) {
1948
+ // Fast swipe - use velocity direction
1949
+ targetPosition = velocity < 0 ? -(screenWidth * 1.07 + 44) : 0;
1950
+ } else {
1951
+ // Slow swipe - use position threshold (snap to nearest half)
1952
+ const fullDistance = screenWidth * 1.07 + 44;
1953
+ const midPoint = -fullDistance / 2;
1954
+
1955
+ // If final position is above midpoint, snap to top, otherwise snap to bottom
1956
+ targetPosition = finalPosition < midPoint ? -fullDistance : 0;
1957
+ }
1958
+
1959
+ // Animate to target position with smooth timing
1960
+ translateY.value = withTiming(targetPosition, {
1961
+ duration: 300,
1962
+ });
1963
+ });
1964
+ const animatedStyle = useAnimatedStyle(() => {
1965
+ return {
1966
+ transform: [
1967
+ {
1968
+ translateY: translateY.value,
1969
+ },
1970
+ ],
1971
+ paddingBottom: translateY.value === 0 ? screenWidth * 1.07 + 44 : 0,
1972
+ };
1973
+ });
1974
+
1975
+ return (
1976
+ <FontProvider
1977
+ fonts={{
1978
+ regular: "Playy-Regular",
1979
+ medium: "Playy-Medium",
1980
+
1981
+ bold: "Playy-Bold",
1982
+
1983
+ SemiCondensedExtraBold: Platform.OS === "android"
1984
+
1985
+ ? "OpenSans_SemiCondensed-ExtraBold"
1986
+
1987
+ : "OpenSansSemiCondensed-ExtraBold"
1988
+
1989
+ }}
1990
+
1991
+ >
1992
+ <View
1993
+ style={{
1994
+ backgroundColor: '#000',
1995
+ }}>
1996
+
1997
+ {renderCancelNextButtons()}
1998
+
1999
+ <Animated.View
2000
+ style={[
2001
+ styles.container,
2002
+ animatedStyle,
2003
+ {
2004
+ height: screenHeight + (screenWidth * 1.07 + 44 - 140),
2005
+ },
2006
+ ]}>
2007
+ <GestureHandlerRootView>
2008
+ {(permissionStatus === 'authorized' ||
2009
+ permissionStatus === 'limited') && (
2010
+ <>
2011
+ <GestureDetector gesture={selectedAssetGesture}>
2012
+ <View>{renderSelectedAssetsHeader()}</View>
2013
+ </GestureDetector>
2014
+
2015
+ <GestureDetector gesture={renderHeaderGesture}>
2016
+ <Animated.View>
2017
+ {renderCustomHeader()}
2018
+ {renderHeader()}
2019
+ </Animated.View>
2020
+ </GestureDetector>
2021
+ </>
2022
+ )}
2023
+
2024
+ <GestureDetector gesture={assetGesture}>
2025
+ {renderAssetsGrid()}
2026
+ </GestureDetector>
2027
+ </GestureHandlerRootView>
2028
+ {renderManageModal()}
2029
+
2030
+ {/* Loading overlay for album picker processing */}
2031
+ {loading && (
2032
+ <View style={styles.loadingOverlay}>
2033
+ <ActivityIndicator size="large" color="#fff" />
2034
+ </View>
2035
+ )}
2036
+ </Animated.View>
2037
+ </View>
2038
+ </FontProvider>
2039
+
2040
+ );
2041
+ },
2042
+ );
2043
+
2044
+ const styles = StyleSheet.create({
2045
+ container: {
2046
+ backgroundColor: '#000',
2047
+ },
2048
+ topBackground: {
2049
+ backgroundColor: '#000',
2050
+ },
2051
+ selectedAssetWrapper: {
2052
+ justifyContent: 'center',
2053
+ alignItems: 'center',
2054
+ backgroundColor: '#000',
2055
+ },
2056
+ navControls: {
2057
+ flexDirection: 'row',
2058
+ justifyContent: 'center',
2059
+ alignItems: 'center',
2060
+ },
2061
+ simpleHeader: {
2062
+ flexDirection: 'row',
2063
+ alignItems: 'center',
2064
+ paddingHorizontal: 12,
2065
+ paddingVertical: 8,
2066
+ backgroundColor: '#000',
2067
+ zIndex: 2,
2068
+ },
2069
+ headerBackButton: {
2070
+ paddingHorizontal: 12,
2071
+ paddingVertical: 6,
2072
+ },
2073
+ mainFlatList: {
2074
+ flex: 1,
2075
+ },
2076
+ mainContentContainer: {
2077
+ flex: 1,
2078
+ minHeight: screenWidth, // Ensure minimum height for content
2079
+ },
2080
+ header: {
2081
+ flexDirection: 'row',
2082
+ justifyContent: 'space-between',
2083
+ alignItems: 'center',
2084
+ paddingHorizontal: 12,
2085
+ paddingVertical: 8,
2086
+ borderTopWidth: 0.5,
2087
+ borderTopColor: 'rgba(255, 255, 255, 0.3)',
2088
+ backgroundColor: '#000',
2089
+ },
2090
+ headerLeft: {
2091
+ flex: 1,
2092
+ },
2093
+ headerTitle: {
2094
+ color: 'rgba(235, 238, 245, 0.60)',
2095
+ },
2096
+ headerSubtitle: {
2097
+ fontSize: 14,
2098
+ color: '#666',
2099
+ marginTop: 2,
2100
+ },
2101
+ headerButtons: {
2102
+ flexDirection: 'row',
2103
+ alignItems: 'center',
2104
+ gap: 8,
2105
+ },
2106
+ toggleButton: {
2107
+ backgroundColor: '#F2F2F2',
2108
+ paddingHorizontal: 12,
2109
+ paddingVertical: 6,
2110
+ borderRadius: 6,
2111
+ borderWidth: 1,
2112
+ borderColor: '#E5E5EA',
2113
+ },
2114
+ toggleButtonActive: {
2115
+ backgroundColor: '#FF9500',
2116
+ borderColor: '#FF9500',
2117
+ },
2118
+ toggleButtonText: {
2119
+ color: '#666',
2120
+ fontSize: 12,
2121
+ fontWeight: '500',
2122
+ },
2123
+ toggleButtonTextActive: {
2124
+ color: 'white',
2125
+ },
2126
+ manageButton: {
2127
+ backgroundColor: 'rgba(120, 120, 128, 0.12)',
2128
+ paddingHorizontal: 14,
2129
+ paddingVertical: 7,
2130
+ borderRadius: 40,
2131
+ },
2132
+ manageButtonText: {
2133
+ color: '#0073FF',
2134
+ },
2135
+ multiSelectToggle: {
2136
+ backgroundColor: '#f0f0f0',
2137
+ paddingHorizontal: 12,
2138
+ paddingVertical: 8,
2139
+ borderRadius: 8,
2140
+ marginRight: 8,
2141
+ borderWidth: 1,
2142
+ borderColor: '#ddd',
2143
+ },
2144
+ multiSelectToggleActive: {
2145
+ backgroundColor: '#007AFF',
2146
+ borderColor: '#007AFF',
2147
+ },
2148
+ multiSelectToggleText: {
2149
+ fontSize: 16,
2150
+ color: '#333',
2151
+ },
2152
+ centerContainer: {
2153
+ flex: 1,
2154
+ justifyContent: 'center',
2155
+ alignItems: 'center',
2156
+ paddingHorizontal: 20,
2157
+ },
2158
+ galleryIcon: {
2159
+ width: 100,
2160
+ height: 100,
2161
+ borderRadius: 50,
2162
+ backgroundColor: '#F0F0F0',
2163
+ justifyContent: 'center',
2164
+ alignItems: 'center',
2165
+ marginBottom: 16,
2166
+ },
2167
+ galleryIconText: {
2168
+ fontSize: 40,
2169
+ },
2170
+ permissionText: {
2171
+ fontSize: 16,
2172
+ color: '#666',
2173
+ textAlign: 'center',
2174
+ },
2175
+ gridContainer: {
2176
+ padding: 6,
2177
+ },
2178
+ assetContainer: {
2179
+ borderRadius: 8,
2180
+ overflow: 'hidden',
2181
+ },
2182
+ assetsGridContainer: {
2183
+ flex: 1,
2184
+ },
2185
+ assetsGridContent: {
2186
+ padding: 12,
2187
+ gap: 8,
2188
+ },
2189
+ assetImage: {
2190
+ width: '100%',
2191
+ height: '100%',
2192
+ },
2193
+ footerLoader: {
2194
+ paddingVertical: 20,
2195
+ alignItems: 'center',
2196
+ },
2197
+ modalOverlay: {
2198
+ flex: 1,
2199
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
2200
+ justifyContent: 'flex-end',
2201
+ },
2202
+ modalContainer: {
2203
+ backgroundColor: '#000',
2204
+ borderTopLeftRadius: 20,
2205
+ borderTopRightRadius: 20,
2206
+ },
2207
+ modalContent: {
2208
+ padding: 20,
2209
+ },
2210
+ modalTitle: {
2211
+ fontSize: 18,
2212
+ fontWeight: '600',
2213
+ textAlign: 'center',
2214
+ marginBottom: 20,
2215
+ color: '#fff',
2216
+ },
2217
+ modalButton: {
2218
+ backgroundColor: '#F2F2F2',
2219
+ paddingVertical: 16,
2220
+ paddingHorizontal: 20,
2221
+ borderRadius: 12,
2222
+ marginVertical: 6,
2223
+ },
2224
+ modalButtonText: {
2225
+ fontSize: 16,
2226
+ fontWeight: '500',
2227
+ color: '#333',
2228
+ marginBottom: 4,
2229
+ },
2230
+ modalButtonSubtext: {
2231
+ fontSize: 14,
2232
+ color: '#666',
2233
+ },
2234
+ cancelButton: {
2235
+ backgroundColor: '#FF3B30',
2236
+ marginTop: 10,
2237
+ },
2238
+ cancelButtonText: {
2239
+ color: 'white',
2240
+ textAlign: 'center',
2241
+ },
2242
+ emptyContainer: {
2243
+ flex: 1,
2244
+ justifyContent: 'center',
2245
+ alignItems: 'center',
2246
+ paddingTop: 50,
2247
+ },
2248
+ emptyText: {
2249
+ fontSize: 16,
2250
+ color: '#666',
2251
+ textAlign: 'center',
2252
+ },
2253
+ // Selected Images Modal Styles
2254
+ selectedImagesContainer: {
2255
+ flex: 1,
2256
+ backgroundColor: '#fff',
2257
+ },
2258
+ selectedImagesHeader: {
2259
+ flexDirection: 'row',
2260
+ alignItems: 'center',
2261
+ justifyContent: 'space-between',
2262
+ paddingHorizontal: 16,
2263
+ paddingVertical: 12,
2264
+ borderBottomWidth: 1,
2265
+ borderBottomColor: '#E5E5EA',
2266
+ backgroundColor: '#F8F8F8',
2267
+ },
2268
+ closeButton: {
2269
+ width: 32,
2270
+ height: 32,
2271
+ borderRadius: 16,
2272
+ backgroundColor: '#E5E5EA',
2273
+ justifyContent: 'center',
2274
+ alignItems: 'center',
2275
+ },
2276
+ closeButtonText: {
2277
+ fontSize: 16,
2278
+ color: '#666',
2279
+ fontWeight: '600',
2280
+ },
2281
+ selectedImagesTitle: {
2282
+ fontSize: 18,
2283
+ fontWeight: '600',
2284
+ color: '#333',
2285
+ },
2286
+ placeholder: {
2287
+ width: 32,
2288
+ height: 32,
2289
+ },
2290
+ selectedImagesScrollView: {
2291
+ flex: 1,
2292
+ },
2293
+ selectedImageItem: {
2294
+ flexDirection: 'row',
2295
+ padding: 16,
2296
+ borderBottomWidth: 1,
2297
+ borderBottomColor: '#F0F0F0',
2298
+ },
2299
+ mediaPreviewContainer: {
2300
+ position: 'relative',
2301
+ marginRight: 16,
2302
+ },
2303
+ selectedImage: {
2304
+ width: 80,
2305
+ height: 80,
2306
+ borderRadius: 8,
2307
+ },
2308
+ videoOverlay: {
2309
+ position: 'absolute',
2310
+ top: 0,
2311
+ left: 0,
2312
+ right: 0,
2313
+ bottom: 0,
2314
+ justifyContent: 'center',
2315
+ alignItems: 'center',
2316
+ backgroundColor: 'rgba(0, 0, 0, 0.3)',
2317
+ borderRadius: 8,
2318
+ },
2319
+ videoPlayIcon: {
2320
+ fontSize: 24,
2321
+ },
2322
+ imageDetails: {
2323
+ flex: 1,
2324
+ justifyContent: 'space-between',
2325
+ },
2326
+ imageDetailTitle: {
2327
+ fontSize: 16,
2328
+ fontWeight: '600',
2329
+ color: '#333',
2330
+ marginBottom: 8,
2331
+ },
2332
+ imageDetailText: {
2333
+ fontSize: 14,
2334
+ color: '#666',
2335
+ marginBottom: 4,
2336
+ },
2337
+ // Selection Mode Styles
2338
+ selectionInfo: {
2339
+ flex: 1,
2340
+ alignItems: 'center',
2341
+ },
2342
+ selectionText: {
2343
+ fontSize: 16,
2344
+ fontWeight: '600',
2345
+ color: '#007AFF',
2346
+ },
2347
+ nextButton: {
2348
+ backgroundColor: '#007AFF',
2349
+ paddingHorizontal: 20,
2350
+ paddingVertical: 8,
2351
+ borderRadius: 8,
2352
+ },
2353
+ nextButtonDisabled: {
2354
+ backgroundColor: '#C7C7CC',
2355
+ },
2356
+ nextButtonText: {
2357
+ color: 'white',
2358
+ fontSize: 16,
2359
+ fontWeight: '600',
2360
+ },
2361
+ nextButtonTextDisabled: {
2362
+ color: '#8E8E93',
2363
+ },
2364
+ selectionCancelButton: {
2365
+ paddingVertical: 8,
2366
+ },
2367
+ selectionCancelButtonText: {
2368
+ color: '#007AFF',
2369
+ fontSize: 16,
2370
+ },
2371
+ // Asset Selection Styles
2372
+ selectedAssetImage: {
2373
+ opacity: 0.6,
2374
+ },
2375
+ selectionIndicator: {
2376
+ position: 'absolute',
2377
+ top: 8,
2378
+ right: 8,
2379
+ zIndex: 1,
2380
+ },
2381
+ selectedIndicator: {
2382
+ width: 24,
2383
+ height: 24,
2384
+ borderRadius: 12,
2385
+ backgroundColor: '#007AFF',
2386
+ justifyContent: 'center',
2387
+ alignItems: 'center',
2388
+ borderWidth: 2,
2389
+ borderColor: '#FFFFFF',
2390
+ },
2391
+ unselectedIndicator: {
2392
+ width: 24,
2393
+ height: 24,
2394
+ borderRadius: 12,
2395
+ backgroundColor: 'rgba(255, 255, 255, 0.8)',
2396
+ borderWidth: 2,
2397
+ borderColor: '#007AFF',
2398
+ },
2399
+ selectionNumber: {
2400
+ color: 'white',
2401
+ fontSize: 12,
2402
+ fontWeight: 'bold',
2403
+ },
2404
+ selectionOverlay: {
2405
+ position: 'absolute',
2406
+ top: 0,
2407
+ left: 0,
2408
+ right: 0,
2409
+ bottom: 0,
2410
+ backgroundColor: 'rgba(0, 122, 255, 0.2)',
2411
+ borderRadius: 4,
2412
+ },
2413
+ // Video indicator styles
2414
+ videoIndicator: {
2415
+ position: 'absolute',
2416
+ bottom: 4,
2417
+ left: 4,
2418
+ flexDirection: 'row',
2419
+ alignItems: 'center',
2420
+ backgroundColor: 'rgba(0, 0, 0, 0.7)',
2421
+ paddingHorizontal: 4,
2422
+ paddingVertical: 2,
2423
+ borderRadius: 4,
2424
+ },
2425
+ videoIcon: {
2426
+ fontSize: 12,
2427
+ marginRight: 2,
2428
+ },
2429
+ videoDuration: {
2430
+ color: 'white',
2431
+ fontSize: 10,
2432
+ fontWeight: '500',
2433
+ },
2434
+ // Selected Assets Header Styles
2435
+ selectedAssetsHeaderContainer: {
2436
+ backgroundColor: '#000',
2437
+ },
2438
+ selectedAssetsContainer: {
2439
+ backgroundColor: '#000',
2440
+ borderTopWidth: 0.6,
2441
+ borderBottomWidth: 0.6,
2442
+ borderColor: 'rgba(255, 255, 255, 0.3)',
2443
+ },
2444
+ selectedAssetsTitle: {
2445
+ fontSize: 16,
2446
+ fontWeight: '600',
2447
+ color: '#333',
2448
+ paddingHorizontal: 16,
2449
+ marginBottom: 8,
2450
+ },
2451
+ selectedAssetsList: {},
2452
+ selectedAssetContainer: {
2453
+ position: 'relative',
2454
+ width: screenWidth,
2455
+ height: screenWidth,
2456
+ backgroundColor: '#000',
2457
+ overflow: 'hidden',
2458
+ },
2459
+ cropperContainer: {
2460
+ position: 'relative',
2461
+ width: screenWidth,
2462
+ height: screenWidth,
2463
+ backgroundColor: '#000',
2464
+ overflow: 'hidden',
2465
+ },
2466
+ cropImageContainer: {
2467
+ width: screenWidth,
2468
+ height: screenWidth,
2469
+ justifyContent: 'center',
2470
+ alignItems: 'center',
2471
+ },
2472
+ cropImage: {
2473
+ width: '100%',
2474
+ height: '100%',
2475
+ resizeMode: 'cover',
2476
+ },
2477
+ cropOverlayContainer: {
2478
+ position: 'absolute',
2479
+ top: 0,
2480
+ left: 0,
2481
+ right: 0,
2482
+ bottom: 0,
2483
+ },
2484
+ cropBorder: {
2485
+ position: 'absolute',
2486
+ backgroundColor: 'transparent',
2487
+ },
2488
+ cropWindow: {
2489
+ position: 'absolute',
2490
+ backgroundColor: 'transparent',
2491
+ },
2492
+ removeButton: {
2493
+ position: 'absolute',
2494
+ top: -4,
2495
+ right: -4,
2496
+ backgroundColor: '#FF3B30',
2497
+ borderRadius: 10,
2498
+ width: 20,
2499
+ height: 20,
2500
+ alignItems: 'center',
2501
+ justifyContent: 'center',
2502
+ borderWidth: 2,
2503
+ borderColor: '#ffffff',
2504
+ },
2505
+ removeButtonText: {
2506
+ color: '#ffffff',
2507
+ fontSize: 12,
2508
+ fontWeight: 'bold',
2509
+ },
2510
+ videoIndicatorSmall: {
2511
+ position: 'absolute',
2512
+ bottom: 4,
2513
+ right: 4,
2514
+ backgroundColor: 'rgba(0, 0, 0, 0.7)',
2515
+ borderRadius: 4,
2516
+ padding: 2,
2517
+ },
2518
+ videoIconSmall: {
2519
+ fontSize: 10,
2520
+ },
2521
+ selectedAssetHeaderImage: {
2522
+ resizeMode: 'cover',
2523
+ },
2524
+ cropOverlay: {
2525
+ position: 'absolute',
2526
+ top: 0,
2527
+ left: 0,
2528
+ right: 0,
2529
+ bottom: 0,
2530
+ pointerEvents: 'none',
2531
+ },
2532
+ gridLine: {
2533
+ position: 'absolute',
2534
+ backgroundColor: 'rgba(255, 255, 255, 0.5)',
2535
+ },
2536
+ gridVertical1: {
2537
+ width: 1,
2538
+ height: '100%',
2539
+ left: '33.33%',
2540
+ },
2541
+ gridVertical2: {
2542
+ width: 1,
2543
+ height: '100%',
2544
+ left: '66.66%',
2545
+ },
2546
+ gridHorizontal1: {
2547
+ width: '100%',
2548
+ height: 1,
2549
+ top: '33.33%',
2550
+ },
2551
+ gridHorizontal2: {
2552
+ width: '100%',
2553
+ height: 1,
2554
+ top: '66.66%',
2555
+ },
2556
+ cropMask: {
2557
+ position: 'absolute',
2558
+ backgroundColor: 'rgba(0, 0, 0, 0.6)',
2559
+ pointerEvents: 'none',
2560
+ },
2561
+ placeholderContainer: {
2562
+ alignItems: 'center',
2563
+ justifyContent: 'center',
2564
+ borderTopWidth: 0.6,
2565
+ borderBottomWidth: 0.6,
2566
+ borderColor: 'rgba(255, 255, 255, 0.3)',
2567
+ },
2568
+ placeholderBox: {
2569
+ width: screenWidth,
2570
+ height: screenWidth * 1.07,
2571
+ backgroundColor: '#000',
2572
+ borderStyle: 'dashed',
2573
+ },
2574
+ // Custom Header Styles
2575
+ customHeader: {
2576
+ flexDirection: 'row',
2577
+ justifyContent: 'space-between',
2578
+ alignItems: 'center',
2579
+ paddingHorizontal: 16,
2580
+ backgroundColor: '#000',
2581
+ paddingVertical: 12,
2582
+ },
2583
+ coverButton: {
2584
+ width: 32,
2585
+ height: 32,
2586
+ backgroundColor: '#333',
2587
+ borderRadius: 4,
2588
+ justifyContent: 'center',
2589
+ alignItems: 'center',
2590
+ },
2591
+ coverButtonText: {
2592
+ fontSize: 16,
2593
+ color: '#fff',
2594
+ },
2595
+ aspectRatioContainer: {
2596
+ flexDirection: 'row',
2597
+ paddingHorizontal: 4,
2598
+ },
2599
+ aspectRatioButton: {
2600
+ paddingHorizontal: 12,
2601
+ },
2602
+ aspectRatioButtonActive: {
2603
+ backgroundColor: '#fff',
2604
+ },
2605
+ aspectRatioText: {
2606
+ fontSize: 12,
2607
+ color: '#fff',
2608
+ fontWeight: '500',
2609
+ },
2610
+ aspectRatioTextActive: {
2611
+ color: '#000',
2612
+ },
2613
+ multiSelectToggleButton: {},
2614
+
2615
+ multiSelectToggleButtonText: {
2616
+ fontSize: 16,
2617
+ color: '#fff',
2618
+ },
2619
+ // Loading overlay styles
2620
+ loadingOverlay: {
2621
+ position: 'absolute',
2622
+ top: 0,
2623
+ left: 0,
2624
+ right: 0,
2625
+ bottom: 0,
2626
+ backgroundColor: 'rgba(0, 0, 0, 0.8)',
2627
+ justifyContent: 'center',
2628
+ alignItems: 'center',
2629
+ zIndex: 9999,
2630
+ },
2631
+ loadingText: {
2632
+ color: '#fff',
2633
+ fontSize: 16,
2634
+ marginTop: 12,
2635
+ textAlign: 'center',
2636
+ },
2637
+ });
2638
+
2639
+ export default MainPhotoGallery;