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.
- package/README.md +236 -0
- package/android/build.gradle +22 -0
- package/android/src/main/AndroidManifest.xml +10 -0
- package/android/src/main/java/com/gallerypicker/imagepicker/ImagePickerModule.java +1968 -0
- package/android/src/main/java/com/gallerypicker/imagepicker/ImagePickerPackage.java +24 -0
- package/index.d.ts +129 -0
- package/index.js +18 -0
- package/ios/ImagePickerModule.h +8 -0
- package/ios/ImagePickerModule.m +876 -0
- package/motorinc-gallery-picker-pro.podspec +21 -0
- package/package.json +63 -0
- package/react-native.config.js +13 -0
- package/src/components/ImageCropper.tsx +433 -0
- package/src/components/MainPhotoGallery.tsx +2639 -0
- package/src/components/PhotoAssetImage.tsx +121 -0
- package/src/modules/ImagePickerModule.ts +117 -0
|
@@ -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;
|