@technotoil/image-video-editor 0.1.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.
Files changed (99) hide show
  1. package/ImageVideoEditor.podspec +21 -0
  2. package/README.md +136 -0
  3. package/android/build.gradle +76 -0
  4. package/android/gradle.properties +5 -0
  5. package/android/src/main/AndroidManifest.xml +13 -0
  6. package/android/src/main/java/com/technotoil/image_videoeditor/FrameGrabberModule.kt +67 -0
  7. package/android/src/main/java/com/technotoil/image_videoeditor/MediaEditorModule.kt +548 -0
  8. package/android/src/main/java/com/technotoil/image_videoeditor/MediaFileUtils.kt +29 -0
  9. package/android/src/main/java/com/technotoil/image_videoeditor/MediaLibraryModule.kt +305 -0
  10. package/android/src/main/java/com/technotoil/image_videoeditor/MediaPackage.kt +26 -0
  11. package/android/src/main/java/com/technotoil/image_videoeditor/MediaPickerModule.kt +111 -0
  12. package/android/src/main/java/com/technotoil/image_videoeditor/MediaPlayerModule.kt +34 -0
  13. package/android/src/main/java/com/technotoil/image_videoeditor/RNCameraViewManager.kt +761 -0
  14. package/android/src/main/java/com/technotoil/image_videoeditor/RNVideoPreviewManager.kt +317 -0
  15. package/ios/PrivacyInfo.xcprivacy +38 -0
  16. package/ios/RNCameraViewManager.m +420 -0
  17. package/ios/RNFrameGrabber.m +61 -0
  18. package/ios/RNMediaEditor.m +905 -0
  19. package/ios/RNMediaLibrary.m +389 -0
  20. package/ios/RNMediaPicker.m +144 -0
  21. package/ios/RNMediaPlayer.m +73 -0
  22. package/ios/RNVideoPreviewManager.m +263 -0
  23. package/ios/frames/film_vintage.png +0 -0
  24. package/ios/frames/floral_gold.png +0 -0
  25. package/ios/frames/minimal_double.png +0 -0
  26. package/ios/frames/polaroid_white.png +0 -0
  27. package/ios/frames/watercolor_floral.png +0 -0
  28. package/lib/module/assets/frames/film_vintage.png +0 -0
  29. package/lib/module/assets/frames/floral_gold.png +0 -0
  30. package/lib/module/assets/frames/minimal_double.png +0 -0
  31. package/lib/module/assets/frames/polaroid_white.png +0 -0
  32. package/lib/module/assets/frames/watercolor_floral.png +0 -0
  33. package/lib/module/components/VideoEditor.js +156 -0
  34. package/lib/module/components/VideoEditor.js.map +1 -0
  35. package/lib/module/index.js +4 -0
  36. package/lib/module/index.js.map +1 -0
  37. package/lib/module/native/CameraView.js +104 -0
  38. package/lib/module/native/CameraView.js.map +1 -0
  39. package/lib/module/native/FrameGrabber.js +13 -0
  40. package/lib/module/native/FrameGrabber.js.map +1 -0
  41. package/lib/module/native/MediaEditor.js +19 -0
  42. package/lib/module/native/MediaEditor.js.map +1 -0
  43. package/lib/module/native/MediaLibrary.js +37 -0
  44. package/lib/module/native/MediaLibrary.js.map +1 -0
  45. package/lib/module/native/MediaPicker.js +13 -0
  46. package/lib/module/native/MediaPicker.js.map +1 -0
  47. package/lib/module/native/MediaPlayer.js +13 -0
  48. package/lib/module/native/MediaPlayer.js.map +1 -0
  49. package/lib/module/native/VideoPreview.js +12 -0
  50. package/lib/module/native/VideoPreview.js.map +1 -0
  51. package/lib/module/package.json +1 -0
  52. package/lib/module/screens/CropScreen.js +1211 -0
  53. package/lib/module/screens/CropScreen.js.map +1 -0
  54. package/lib/module/screens/EditorScreen.js +5752 -0
  55. package/lib/module/screens/EditorScreen.js.map +1 -0
  56. package/lib/module/screens/ExportScreen.js +289 -0
  57. package/lib/module/screens/ExportScreen.js.map +1 -0
  58. package/lib/module/screens/GalleryScreen.js +505 -0
  59. package/lib/module/screens/GalleryScreen.js.map +1 -0
  60. package/lib/module/screens/PickScreen.js +1195 -0
  61. package/lib/module/screens/PickScreen.js.map +1 -0
  62. package/lib/module/types.js +2 -0
  63. package/lib/module/types.js.map +1 -0
  64. package/lib/typescript/src/components/VideoEditor.d.ts +13 -0
  65. package/lib/typescript/src/index.d.ts +2 -0
  66. package/lib/typescript/src/native/CameraView.d.ts +23 -0
  67. package/lib/typescript/src/native/FrameGrabber.d.ts +2 -0
  68. package/lib/typescript/src/native/MediaEditor.d.ts +3 -0
  69. package/lib/typescript/src/native/MediaLibrary.d.ts +16 -0
  70. package/lib/typescript/src/native/MediaPicker.d.ts +2 -0
  71. package/lib/typescript/src/native/MediaPlayer.d.ts +1 -0
  72. package/lib/typescript/src/native/VideoPreview.d.ts +19 -0
  73. package/lib/typescript/src/screens/CropScreen.d.ts +9 -0
  74. package/lib/typescript/src/screens/EditorScreen.d.ts +10 -0
  75. package/lib/typescript/src/screens/ExportScreen.d.ts +9 -0
  76. package/lib/typescript/src/screens/GalleryScreen.d.ts +8 -0
  77. package/lib/typescript/src/screens/PickScreen.d.ts +13 -0
  78. package/lib/typescript/src/types.d.ts +58 -0
  79. package/package.json +101 -0
  80. package/src/assets/frames/film_vintage.png +0 -0
  81. package/src/assets/frames/floral_gold.png +0 -0
  82. package/src/assets/frames/minimal_double.png +0 -0
  83. package/src/assets/frames/polaroid_white.png +0 -0
  84. package/src/assets/frames/watercolor_floral.png +0 -0
  85. package/src/components/VideoEditor.tsx +182 -0
  86. package/src/index.tsx +2 -0
  87. package/src/native/CameraView.tsx +95 -0
  88. package/src/native/FrameGrabber.ts +21 -0
  89. package/src/native/MediaEditor.ts +33 -0
  90. package/src/native/MediaLibrary.ts +69 -0
  91. package/src/native/MediaPicker.ts +17 -0
  92. package/src/native/MediaPlayer.ts +16 -0
  93. package/src/native/VideoPreview.tsx +20 -0
  94. package/src/screens/CropScreen.tsx +968 -0
  95. package/src/screens/EditorScreen.tsx +4517 -0
  96. package/src/screens/ExportScreen.tsx +282 -0
  97. package/src/screens/GalleryScreen.tsx +412 -0
  98. package/src/screens/PickScreen.tsx +1094 -0
  99. package/src/types.ts +58 -0
@@ -0,0 +1,1094 @@
1
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
2
+ import {
3
+ Alert,
4
+ Animated,
5
+ FlatList,
6
+ Image,
7
+ Modal,
8
+ PermissionsAndroid,
9
+ Platform,
10
+ Pressable,
11
+ ScrollView,
12
+ StyleSheet,
13
+ Text,
14
+ View,
15
+ } from 'react-native';
16
+ import Ionicons from 'react-native-vector-icons/Ionicons';
17
+ import ImagePicker from 'react-native-image-crop-picker';
18
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
19
+ import { CameraView, CameraViewRef } from '../native/CameraView';
20
+ import { Album, exportAsset, listAlbums, listMedia, requestMediaAccess } from '../native/MediaLibrary';
21
+ import { VideoPreview } from '../native/VideoPreview';
22
+ import type { MediaItem } from '../types';
23
+
24
+ const TABS: Array<'GALLERY' | 'PHOTO' | 'VIDEO'> = ['GALLERY', 'PHOTO', 'VIDEO'];
25
+ const POST_TYPES: Array<'POST' | 'STORY' | 'REEL'> = ['POST', 'STORY', 'REEL'];
26
+
27
+
28
+ const CameraIcon = () => (
29
+ <View style={{ width: 30, height: 22, borderWidth: 2, borderColor: '#fff', borderRadius: 4, alignItems: 'center', justifyContent: 'center', position: 'relative' }}>
30
+ <View style={{ width: 10, height: 10, borderRadius: 5, borderWidth: 2, borderColor: '#fff' }} />
31
+ <View style={{ width: 6, height: 3, backgroundColor: '#fff', position: 'absolute', top: -4, left: 4, borderRadius: 1 }} />
32
+ </View>
33
+ );
34
+
35
+ function formatDuration(ms?: number) {
36
+ if (!ms || ms <= 0) return '';
37
+ const total = Math.floor(ms / 1000);
38
+ const m = Math.floor(total / 60);
39
+ const s = total % 60;
40
+ return `${m}:${s.toString().padStart(2, '0')}`;
41
+ }
42
+
43
+ export function PickScreen({
44
+ items,
45
+ onPicked,
46
+ onNext,
47
+ headerTitle = 'New post',
48
+ customCancelIcon,
49
+ onCancelPress,
50
+ cameraModes = ['POST', 'STORY', 'REEL'],
51
+ onCameraModeChange,
52
+ defaultCameraMode,
53
+ }: {
54
+ items: MediaItem[];
55
+ onPicked: (items: MediaItem[]) => void;
56
+ onNext: (picked: MediaItem[]) => void;
57
+ headerTitle?: string;
58
+ customCancelIcon?: React.ReactNode;
59
+ onCancelPress?: () => void;
60
+ cameraModes?: string[];
61
+ onCameraModeChange?: (mode: string) => void;
62
+ defaultCameraMode?: string;
63
+ }) {
64
+ const insets = useSafeAreaInsets();
65
+ const [tab, setTab] = useState<'GALLERY' | 'PHOTO' | 'VIDEO'>('GALLERY');
66
+ const [activeAlbum, setActiveAlbum] = useState<Album>({ id: 'all', title: 'Recents' });
67
+ const [showAlbumPicker, setShowAlbumPicker] = useState(false);
68
+ const [postType, setPostType] = useState<'POST' | 'STORY' | 'REEL'>('POST');
69
+ const [multiSelect, setMultiSelect] = useState(false);
70
+ const [selectedItems, setSelectedItems] = useState<MediaItem[]>([]);
71
+ const [selectedMedia, setSelectedMedia] = useState<MediaItem | null>(null);
72
+ const [library, setLibrary] = useState<MediaItem[]>([]);
73
+ const [albums, setAlbums] = useState<Album[]>([]);
74
+ const [loading, setLoading] = useState(false);
75
+ const [previewUri, setPreviewUri] = useState<string | null>(null);
76
+ const [cropMode, setCropMode] = useState<'1:1' | 'original'>('1:1');
77
+ const [videoPaused, setVideoPaused] = useState(false);
78
+ const scaleAnim = useRef(new Animated.Value(1)).current;
79
+
80
+ const [showCustomCamera, setShowCustomCamera] = useState(false);
81
+ const [facing, setFacing] = useState<'front' | 'back'>('front');
82
+ const [flashMode, setFlashMode] = useState<'on' | 'off'>('off');
83
+ const [isRecording, setIsRecording] = useState(false);
84
+ const isRecordingRef = useRef(false);
85
+ const cameraRef = useRef<CameraViewRef>(null);
86
+ const [activeCameraMode, setActiveCameraMode] = useState<string>(() => {
87
+ if (defaultCameraMode) {
88
+ const matched = cameraModes.find(m => m.toUpperCase() === defaultCameraMode.toUpperCase());
89
+ if (matched) return matched;
90
+ }
91
+ return cameraModes.includes('STORY') ? 'STORY' : (cameraModes[0] || 'STORY');
92
+ });
93
+
94
+ useEffect(() => {
95
+ onCameraModeChange?.(activeCameraMode);
96
+ }, [activeCameraMode, onCameraModeChange]);
97
+
98
+ const handleCameraMediaCaptured = (uri: string, type: 'image' | 'video', width: number, height: number, durationMs?: number) => {
99
+ setShowCustomCamera(false);
100
+ const item: MediaItem = {
101
+ id: 'camera_' + Date.now(),
102
+ uri: uri,
103
+ type: type,
104
+ thumbnailUri: uri,
105
+ width: width,
106
+ height: height,
107
+ durationMs: durationMs,
108
+ };
109
+ setLibrary((prev) => [item, ...prev]);
110
+ setSelectedMedia(item);
111
+ setSelectedItems([item]);
112
+ onPicked([item]);
113
+ onNext([item]);
114
+ };
115
+
116
+ const handlePress = async () => {
117
+ if (isRecordingRef.current) {
118
+ return;
119
+ }
120
+ try {
121
+ const photo = await cameraRef.current?.capturePhoto();
122
+ if (photo) {
123
+ handleCameraMediaCaptured(photo.uri, 'image', photo.width, photo.height);
124
+ }
125
+ } catch (err: any) {
126
+ console.error('PickScreen: capturePhoto error:', err);
127
+ Alert.alert('Capture Error', err?.message ?? 'Failed to capture photo');
128
+ }
129
+ };
130
+
131
+ const handleLongPress = async () => {
132
+ try {
133
+ isRecordingRef.current = true;
134
+ setIsRecording(true);
135
+ await cameraRef.current?.startRecording();
136
+ } catch (err: any) {
137
+ isRecordingRef.current = false;
138
+ setIsRecording(false);
139
+ Alert.alert('Recording Error', err?.message ?? 'Failed to start recording');
140
+ }
141
+ };
142
+
143
+ const handlePressOut = async () => {
144
+ if (!isRecordingRef.current) return;
145
+ try {
146
+ const video = await cameraRef.current?.stopRecording();
147
+ isRecordingRef.current = false;
148
+ setIsRecording(false);
149
+ if (video) {
150
+ handleCameraMediaCaptured(video.uri, 'video', video.width, video.height, video.durationMs);
151
+ }
152
+ } catch (err: any) {
153
+ isRecordingRef.current = false;
154
+ setIsRecording(false);
155
+ Alert.alert('Recording Error', err?.message ?? 'Failed to stop recording');
156
+ }
157
+ };
158
+
159
+ const loadMedia = async (albumId?: string) => {
160
+ try {
161
+ setLoading(true);
162
+ const assets = await listMedia({
163
+ limit: 200,
164
+ offset: 0,
165
+ type: 'all',
166
+ albumId: albumId === 'all' ? undefined : albumId,
167
+ });
168
+ setLibrary(assets);
169
+ if (assets[0] && !multiSelect) {
170
+ setSelectedMedia(assets[0]);
171
+ }
172
+ } catch (err: any) {
173
+ Alert.alert('Library error', err?.message ?? 'Failed to load library.');
174
+ } finally {
175
+ setLoading(false);
176
+ }
177
+ };
178
+
179
+ useEffect(() => {
180
+ (async () => {
181
+ try {
182
+ const ok = await requestMediaAccess();
183
+ if (!ok) {
184
+ Alert.alert('Permission needed', 'Allow photo access to continue.');
185
+ return;
186
+ }
187
+ const fetchedAlbums = await listAlbums();
188
+ setAlbums([{ id: 'all', title: 'Recents' }, ...fetchedAlbums]);
189
+ loadMedia('all');
190
+ } catch (err: any) {
191
+ console.error('Initial load failed', err);
192
+ }
193
+ })();
194
+ }, []);
195
+
196
+ const filtered = useMemo(() => {
197
+ let list = library;
198
+ if (tab === 'PHOTO') list = library.filter((i) => i.type === 'image');
199
+ else if (tab === 'VIDEO') list = library.filter((i) => i.type === 'video');
200
+
201
+ const cameraItem: MediaItem = {
202
+ id: 'camera_trigger',
203
+ uri: 'camera_trigger',
204
+ type: 'image',
205
+ };
206
+ return [cameraItem, ...list];
207
+ }, [library, tab]);
208
+
209
+ useEffect(() => {
210
+ let cancelled = false;
211
+ setVideoPaused(false);
212
+ (async () => {
213
+ if (!selectedMedia) {
214
+ setPreviewUri(null);
215
+ return;
216
+ }
217
+ if (!selectedMedia.uri.startsWith('ph://') && !selectedMedia.uri.startsWith('content://')) {
218
+ setPreviewUri(selectedMedia.uri);
219
+ return;
220
+ }
221
+ try {
222
+ const fileUri = await exportAsset(selectedMedia.id);
223
+ if (!cancelled) setPreviewUri(fileUri);
224
+ } catch {
225
+ if (!cancelled) setPreviewUri(null);
226
+ }
227
+ })();
228
+ return () => {
229
+ cancelled = true;
230
+ };
231
+ }, [selectedMedia]);
232
+
233
+ const playableUri = useMemo(() => {
234
+ if (!selectedMedia) return null;
235
+ if (previewUri && !previewUri.startsWith('ph://') && !previewUri.startsWith('content://')) return previewUri;
236
+ if (selectedMedia.uri && !selectedMedia.uri.startsWith('ph://') && !selectedMedia.uri.startsWith('content://')) return selectedMedia.uri;
237
+ return null;
238
+ }, [previewUri, selectedMedia]);
239
+
240
+ const toggleMultiSelect = () => {
241
+ setMultiSelect((v) => !v);
242
+ setSelectedItems([]);
243
+ };
244
+
245
+ const handleSelectItem = (item: MediaItem) => {
246
+ setSelectedMedia(item);
247
+ if (multiSelect) {
248
+ setSelectedItems((prev) => {
249
+ const exists = prev.find((i) => i.id === item.id);
250
+ if (exists) return prev.filter((i) => i.id !== item.id);
251
+ if (prev.length >= 10) return prev;
252
+ return [...prev, item];
253
+ });
254
+ }
255
+
256
+ Animated.sequence([
257
+ Animated.timing(scaleAnim, { toValue: 0.97, duration: 80, useNativeDriver: true }),
258
+ Animated.timing(scaleAnim, { toValue: 1, duration: 80, useNativeDriver: true }),
259
+ ]).start();
260
+ };
261
+
262
+ const handleLongPressItem = (item: MediaItem) => {
263
+ if (!multiSelect) {
264
+ setMultiSelect(true);
265
+ setSelectedItems([item]);
266
+ setSelectedMedia(item);
267
+
268
+ Animated.sequence([
269
+ Animated.timing(scaleAnim, { toValue: 0.97, duration: 80, useNativeDriver: true }),
270
+ Animated.timing(scaleAnim, { toValue: 1, duration: 80, useNativeDriver: true }),
271
+ ]).start();
272
+ }
273
+ };
274
+
275
+ const openImageCropPickerCamera = async (type: 'photo' | 'video') => {
276
+ try {
277
+ const result = await ImagePicker.openCamera({
278
+ mediaType: type,
279
+ });
280
+ if (result) {
281
+ const item: MediaItem = {
282
+ id: 'camera_' + Date.now(),
283
+ uri: result.path,
284
+ type: type === 'photo' ? 'image' : 'video',
285
+ thumbnailUri: result.path,
286
+ width: result.width,
287
+ height: result.height,
288
+ durationMs: type === 'video' ? ((result as any).duration || 10000) : undefined,
289
+ };
290
+ setLibrary((prev) => [item, ...prev]);
291
+ setSelectedMedia(item);
292
+ setSelectedItems([item]);
293
+ onPicked([item]);
294
+ onNext([item]);
295
+ }
296
+ } catch (err: any) {
297
+ if (err?.message !== 'User cancelled image selection' && err?.message !== 'User cancelled image selection.') {
298
+ Alert.alert('Camera error', err?.message ?? 'Failed to open camera.');
299
+ }
300
+ }
301
+ };
302
+
303
+ const handleOpenCamera = async () => {
304
+ if (Platform.OS === 'android') {
305
+ try {
306
+ const cameraGranted = await PermissionsAndroid.request(
307
+ PermissionsAndroid.PERMISSIONS.CAMERA,
308
+ {
309
+ title: 'Camera Permission',
310
+ message: 'App needs access to your camera to take photos and record videos.',
311
+ buttonNeutral: 'Ask Me Later',
312
+ buttonNegative: 'Cancel',
313
+ buttonPositive: 'OK',
314
+ }
315
+ );
316
+
317
+ const audioGranted = await PermissionsAndroid.request(
318
+ PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
319
+ {
320
+ title: 'Microphone Permission',
321
+ message: 'App needs access to your microphone to record audio for videos.',
322
+ buttonNeutral: 'Ask Me Later',
323
+ buttonNegative: 'Cancel',
324
+ buttonPositive: 'OK',
325
+ }
326
+ );
327
+
328
+ if (
329
+ cameraGranted !== PermissionsAndroid.RESULTS.GRANTED ||
330
+ audioGranted !== PermissionsAndroid.RESULTS.GRANTED
331
+ ) {
332
+ Alert.alert('Permissions Required', 'Camera and Microphone permissions are required to use the custom camera.');
333
+ return;
334
+ }
335
+ } catch (err) {
336
+ console.warn(err);
337
+ return;
338
+ }
339
+ }
340
+ setFacing('front');
341
+ setShowCustomCamera(true);
342
+ };
343
+
344
+ const getSelectionIndex = (id: string) => {
345
+ const idx = selectedItems.findIndex((i) => i.id === id);
346
+ return idx === -1 ? null : idx + 1;
347
+ };
348
+
349
+ const handleNext = () => {
350
+ const picked = multiSelect ? selectedItems : selectedMedia ? [selectedMedia] : [];
351
+ if (picked.length === 0) {
352
+ Alert.alert('Select media', 'Choose at least one item.');
353
+ return;
354
+ }
355
+ onPicked(picked);
356
+ onNext(picked);
357
+ };
358
+
359
+ const renderThumb = ({ item }: { item: MediaItem }) => {
360
+ if (item.id === 'camera_trigger') {
361
+ return (
362
+ <Pressable style={styles.cameraGridContainer} onPress={handleOpenCamera}>
363
+ <View style={styles.cameraGridBox}>
364
+ <CameraIcon />
365
+ </View>
366
+ </Pressable>
367
+ );
368
+ }
369
+
370
+ const thumbUri = item.thumbnailUri;
371
+ const selIdx = getSelectionIndex(item.id);
372
+ const isActive = multiSelect ? !!selIdx : selectedMedia?.id === item.id;
373
+
374
+ return (
375
+ <Pressable
376
+ style={styles.thumbContainer}
377
+ onPress={() => handleSelectItem(item)}
378
+ onLongPress={() => handleLongPressItem(item)}
379
+ >
380
+ {thumbUri ? (
381
+ <Image source={{ uri: thumbUri }} style={styles.thumb} />
382
+ ) : (
383
+ <View style={[styles.thumb, styles.thumbFallback]} />
384
+ )}
385
+
386
+ {isActive && !multiSelect && <View style={styles.activeOverlay} />}
387
+
388
+ {multiSelect && (
389
+ <View style={[styles.multiOverlay, isActive && styles.multiOverlayActive]}>
390
+ {isActive ? (
391
+ <View style={styles.selectionBadge}>
392
+ <Text style={styles.selectionNumber}>{selIdx}</Text>
393
+ </View>
394
+ ) : (
395
+ <View style={styles.emptyBadge} />
396
+ )}
397
+ </View>
398
+ )}
399
+
400
+ {item.type === 'video' && (
401
+ <>
402
+ <View style={styles.videoPlayBadge}>
403
+ <Ionicons name="play" size={10} color="#fff" />
404
+ </View>
405
+ <View style={styles.videoBadge}>
406
+ <Text style={styles.videoDuration}>{formatDuration(item.durationMs)}</Text>
407
+ </View>
408
+ </>
409
+ )}
410
+ </Pressable>
411
+ );
412
+ };
413
+
414
+ return (
415
+ <View style={styles.container}>
416
+
417
+ <View style={styles.header}>
418
+ <Pressable onPress={() => {
419
+ if (onCancelPress) {
420
+ onCancelPress();
421
+ } else {
422
+ onNext([]);
423
+ }
424
+ }}>
425
+ {customCancelIcon ? (
426
+ customCancelIcon
427
+ ) : (
428
+ <Ionicons name="close" size={22} color="#fff" />
429
+ )}
430
+ </Pressable>
431
+
432
+ <Text style={styles.headerTitle}>{headerTitle}</Text>
433
+
434
+ <Pressable style={styles.nextBtn} onPress={handleNext}>
435
+ <Text style={styles.nextText}>Next</Text>
436
+ </Pressable>
437
+ </View>
438
+
439
+ <Modal
440
+ visible={showAlbumPicker}
441
+ transparent
442
+ animationType="slide"
443
+ onRequestClose={() => setShowAlbumPicker(false)}
444
+ >
445
+ {/* Backdrop */}
446
+ <Pressable style={styles.albumSheetBackdrop} onPress={() => setShowAlbumPicker(false)} />
447
+
448
+ {/* Sheet */}
449
+ <View style={styles.albumSheet}>
450
+ {/* Handle */}
451
+ <View style={styles.albumSheetHandle} />
452
+
453
+ {/* Title */}
454
+ <Text style={styles.albumSheetTitle}>Select Album</Text>
455
+
456
+ <ScrollView bounces={false}>
457
+ {albums.map((album) => (
458
+ <Pressable
459
+ key={album.id}
460
+ style={styles.albumSheetOption}
461
+ onPress={() => {
462
+ setActiveAlbum(album);
463
+ setShowAlbumPicker(false);
464
+ loadMedia(album.id);
465
+ }}
466
+ >
467
+ <Text style={[
468
+ styles.albumSheetOptionText,
469
+ activeAlbum.id === album.id && styles.albumSheetOptionActive,
470
+ ]}>
471
+ {album.title}
472
+ </Text>
473
+ {activeAlbum.id === album.id && (
474
+ <Ionicons name="checkmark" size={20} color="#3b82f6" />
475
+ )}
476
+ </Pressable>
477
+ ))}
478
+ </ScrollView>
479
+ </View>
480
+ </Modal>
481
+
482
+ <Animated.View style={[styles.preview, { transform: [{ scale: scaleAnim }] }]}>
483
+ {selectedMedia?.type === 'video' ? (
484
+ <Pressable style={styles.previewImage} onPress={() => setVideoPaused((v) => !v)}>
485
+ {playableUri ? (
486
+ <VideoPreview
487
+ uri={playableUri}
488
+ paused={videoPaused}
489
+ muted={false}
490
+ style={styles.previewImage}
491
+ resizeMode={cropMode === '1:1' ? 'cover' : 'contain'}
492
+ />
493
+ ) : (
494
+ <Image
495
+ source={{ uri: selectedMedia.thumbnailUri || selectedMedia.uri }}
496
+ style={styles.previewImage}
497
+ resizeMode="cover"
498
+ />
499
+ )}
500
+ <View style={styles.previewOverlay}>
501
+ <View style={styles.playPauseCircle}>
502
+ <Ionicons name={videoPaused ? 'play' : 'pause'} size={24} color="#fff" />
503
+ </View>
504
+ </View>
505
+ </Pressable>
506
+ ) : (
507
+ <Image
508
+ source={{ uri: previewUri ?? selectedMedia?.uri }}
509
+ style={[styles.previewImage, cropMode === '1:1' ? styles.squareCrop : styles.originalCrop]}
510
+ resizeMode={cropMode === '1:1' ? 'cover' : 'contain'}
511
+ />
512
+ )}
513
+
514
+ <View style={styles.previewControls}>
515
+ <Pressable
516
+ style={styles.cropToggle}
517
+ onPress={() => setCropMode((v) => (v === '1:1' ? 'original' : '1:1'))}
518
+ >
519
+ <Ionicons
520
+ name={cropMode === '1:1' ? 'square-outline' : 'expand-outline'}
521
+ size={20}
522
+ color="#fff"
523
+ />
524
+ </Pressable>
525
+ </View>
526
+ </Animated.View>
527
+
528
+ <View style={styles.albumRow}>
529
+ <Pressable
530
+ style={styles.albumSelector}
531
+ onPress={() => setShowAlbumPicker((v) => !v)}
532
+ >
533
+ <Text style={styles.albumTitle}>{activeAlbum.title}</Text>
534
+ <Ionicons name="chevron-down" size={16} color="#fff" style={{ marginLeft: 6 }} />
535
+ </Pressable>
536
+
537
+ <Pressable style={[styles.multiSelectBtn, multiSelect && styles.multiSelectBtnActive]} onPress={toggleMultiSelect}>
538
+ <Text style={[styles.multiSelectText, multiSelect && styles.multiSelectTextActive]}>{multiSelect ? 'Done' : 'Select'}</Text>
539
+ </Pressable>
540
+ </View>
541
+
542
+ <FlatList
543
+ data={filtered}
544
+ keyExtractor={(item) => item.id}
545
+ numColumns={4}
546
+ style={styles.libraryList}
547
+ renderItem={renderThumb}
548
+ contentContainerStyle={styles.grid}
549
+ ListEmptyComponent={
550
+ <View style={styles.empty}>
551
+ <Text style={styles.emptyText}>{loading ? 'Loading…' : 'No media found'}</Text>
552
+ </View>
553
+ }
554
+ />
555
+
556
+ <View style={styles.tabBar}>
557
+ {TABS.map((t) => (
558
+ <Pressable key={t} onPress={() => setTab(t)}>
559
+ <Text style={[styles.tab, tab === t && styles.tabActive]}>{t}</Text>
560
+ </Pressable>
561
+ ))}
562
+ </View>
563
+
564
+ {/* <View style={styles.postTypeBar}>
565
+ {POST_TYPES.map((t) => (
566
+ <Pressable
567
+ key={t}
568
+ style={[styles.postTypeChip, postType === t && styles.postTypeChipActive]}
569
+ onPress={() => setPostType(t)}
570
+ >
571
+ <Text style={[styles.postTypeText, postType === t && styles.postTypeTextActive]}>
572
+ {t === 'POST' ? 'POST' : t === 'STORY' ? 'STORY' : 'REEL'}
573
+ </Text>
574
+ </Pressable>
575
+ ))}
576
+ </View> */}
577
+
578
+ <Modal
579
+ visible={showCustomCamera}
580
+ animationType="slide"
581
+ transparent={false}
582
+ onRequestClose={() => setShowCustomCamera(false)}
583
+ >
584
+ <View style={styles.cameraContainer}>
585
+ <CameraView
586
+ ref={cameraRef}
587
+ facing={facing}
588
+ flashMode={facing === 'front' ? 'off' : flashMode}
589
+ style={StyleSheet.absoluteFillObject}
590
+ />
591
+
592
+ {/* Rule of Thirds Grid Overlay */}
593
+ <View style={styles.cameraGridOverlay} pointerEvents="none">
594
+ <View style={styles.gridRow}>
595
+ <View style={styles.gridCell} />
596
+ <View style={[styles.gridCell, styles.gridCellMiddleCol]} />
597
+ <View style={styles.gridCell} />
598
+ </View>
599
+ <View style={[styles.gridRow, styles.gridRowMiddleRow]}>
600
+ <View style={styles.gridCell} />
601
+ <View style={[styles.gridCell, styles.gridCellMiddleCol]} />
602
+ <View style={styles.gridCell} />
603
+ </View>
604
+ <View style={styles.gridRow}>
605
+ <View style={styles.gridCell} />
606
+ <View style={[styles.gridCell, styles.gridCellMiddleCol]} />
607
+ <View style={styles.gridCell} />
608
+ </View>
609
+ </View>
610
+
611
+ {/* Top Controls */}
612
+ <View style={[styles.cameraHeader, { top: Math.max(insets.top, 16) }]}>
613
+ <Pressable style={styles.cameraCloseBtn} onPress={() => setShowCustomCamera(false)}>
614
+ <Ionicons name="close" size={22} color="#fff" />
615
+ </Pressable>
616
+
617
+ <Pressable
618
+ style={[styles.cameraFlashContainer, facing === 'front' && { opacity: 0.3 }]}
619
+ onPress={() => setFlashMode((f) => (f === 'on' ? 'off' : 'on'))}
620
+ disabled={facing === 'front'}
621
+ >
622
+ <Ionicons
623
+ name={flashMode === 'on' && facing === 'back' ? 'flash' : 'flash-off'}
624
+ size={22}
625
+ color={flashMode === 'on' && facing === 'back' ? '#FFD700' : 'rgba(255,255,255,0.4)'}
626
+ />
627
+ </Pressable>
628
+ </View>
629
+
630
+ {/* Bottom Controls */}
631
+ <View style={[styles.cameraFooter, { bottom: Math.max(insets.bottom + 60, 60) }]}>
632
+ <View style={styles.cameraGalleryPreview}>
633
+ {library.length > 1 && library[1].thumbnailUri ? (
634
+ <Image source={{ uri: library[1].thumbnailUri }} style={styles.cameraGalleryThumb} />
635
+ ) : (
636
+ <View style={styles.cameraGalleryThumbPlaceholder} />
637
+ )}
638
+ </View>
639
+
640
+ <Pressable
641
+ style={[styles.captureRing, isRecording && styles.captureRingRecording]}
642
+ onPress={handlePress}
643
+ onLongPress={handleLongPress}
644
+ onPressOut={handlePressOut}
645
+ delayLongPress={200}
646
+ >
647
+ <View style={[styles.captureButton, isRecording && styles.captureButtonRecording]} />
648
+ </Pressable>
649
+
650
+ <Pressable
651
+ style={styles.flipCameraBtn}
652
+ onPress={() => setFacing((f) => (f === 'front' ? 'back' : 'front'))}
653
+ >
654
+ <Ionicons name="camera-reverse-outline" size={26} color="#fff" />
655
+ </Pressable>
656
+ </View>
657
+
658
+ {/* Bottom Screen Mode Tabs */}
659
+ <View style={[styles.cameraModeBar, { bottom: Math.max(insets.bottom + 12, 16) }]}>
660
+ <ScrollView
661
+ horizontal
662
+ showsHorizontalScrollIndicator={false}
663
+ contentContainerStyle={styles.cameraModeScrollContainer}
664
+ >
665
+ {cameraModes.map((mode) => {
666
+ const isActive = mode === activeCameraMode;
667
+ return (
668
+ <Pressable key={mode} onPress={() => setActiveCameraMode(mode)}>
669
+ <Text style={isActive ? styles.cameraModeTextActive : styles.cameraModeTextInactive}>
670
+ {mode}
671
+ </Text>
672
+ </Pressable>
673
+ );
674
+ })}
675
+ </ScrollView>
676
+ </View>
677
+ </View>
678
+ </Modal>
679
+ </View>
680
+ );
681
+ }
682
+
683
+ const styles = StyleSheet.create({
684
+ container: { flex: 1, backgroundColor: '#0b0f1a' },
685
+ header: {
686
+ flexDirection: 'row',
687
+ justifyContent: 'space-between',
688
+ alignItems: 'center',
689
+ paddingHorizontal: 16,
690
+ paddingVertical: 12,
691
+ borderBottomWidth: 0,
692
+ backgroundColor: '#0b0f1a',
693
+ zIndex: 10,
694
+ elevation: 10,
695
+ },
696
+ headerTitle: { color: '#fff', fontWeight: '700', fontSize: 18 },
697
+ headerCancel: { fontSize: 20, color: '#fff' },
698
+ nextBtn: {
699
+ backgroundColor: '#2563eb',
700
+ paddingHorizontal: 14,
701
+ paddingVertical: 8,
702
+ borderRadius: 20,
703
+ },
704
+ nextText: { color: '#fff', fontWeight: '700' },
705
+ albumDropdown: {
706
+ borderBottomWidth: 1,
707
+ borderBottomColor: '#111827',
708
+ backgroundColor: '#0b0f1a',
709
+ },
710
+ albumOption: {
711
+ flexDirection: 'row',
712
+ justifyContent: 'space-between',
713
+ paddingHorizontal: 16,
714
+ paddingVertical: 10,
715
+ },
716
+ albumOptionText: { fontSize: 14, color: '#e5e7eb' },
717
+ albumOptionActive: { fontWeight: '700' },
718
+ checkmark: { color: '#2563eb', fontWeight: '700' },
719
+ // Bottom sheet styles
720
+ albumSheetBackdrop: {
721
+ ...StyleSheet.absoluteFillObject,
722
+ backgroundColor: 'rgba(0,0,0,0.55)',
723
+ },
724
+ albumSheet: {
725
+ position: 'absolute',
726
+ bottom: 0,
727
+ left: 0,
728
+ right: 0,
729
+ backgroundColor: '#161b2e',
730
+ borderTopLeftRadius: 20,
731
+ borderTopRightRadius: 20,
732
+ paddingBottom: 32,
733
+ maxHeight: '60%',
734
+ },
735
+ albumSheetHandle: {
736
+ width: 40,
737
+ height: 4,
738
+ borderRadius: 2,
739
+ backgroundColor: '#3f4a6e',
740
+ alignSelf: 'center',
741
+ marginTop: 12,
742
+ marginBottom: 6,
743
+ },
744
+ albumSheetTitle: {
745
+ color: '#fff',
746
+ fontSize: 16,
747
+ fontWeight: '700',
748
+ textAlign: 'center',
749
+ paddingVertical: 12,
750
+ borderBottomWidth: 1,
751
+ borderBottomColor: '#1f2a45',
752
+ },
753
+ albumSheetOption: {
754
+ flexDirection: 'row',
755
+ justifyContent: 'space-between',
756
+ alignItems: 'center',
757
+ paddingHorizontal: 20,
758
+ paddingVertical: 16,
759
+ borderBottomWidth: 1,
760
+ borderBottomColor: '#1a2240',
761
+ },
762
+ albumSheetOptionText: {
763
+ fontSize: 15,
764
+ color: '#cbd5e1',
765
+ },
766
+ albumSheetOptionActive: {
767
+ color: '#fff',
768
+ fontWeight: '700',
769
+ },
770
+ albumSheetCheckmark: {
771
+ color: '#3b82f6',
772
+ fontSize: 18,
773
+ fontWeight: '700',
774
+ },
775
+ preview: { width: '100%', height: 420, backgroundColor: '#0f172a', overflow: 'hidden' },
776
+ previewImage: { width: '100%', height: '100%', overflow: 'hidden' },
777
+ squareCrop: { height: '100%' },
778
+ originalCrop: { height: '100%' },
779
+ previewControls: {
780
+ position: 'absolute',
781
+ left: 16,
782
+ right: 16,
783
+ bottom: 16,
784
+ flexDirection: 'row',
785
+ justifyContent: 'flex-start',
786
+ alignItems: 'center',
787
+ },
788
+ cropToggle: {
789
+ backgroundColor: 'rgba(0,0,0,0.65)',
790
+ width: 34,
791
+ height: 34,
792
+ borderRadius: 17,
793
+ alignItems: 'center',
794
+ justifyContent: 'center',
795
+ },
796
+ cropIcon: { color: '#fff', fontSize: 16 },
797
+ albumRow: {
798
+ flexDirection: 'row',
799
+ justifyContent: 'space-between',
800
+ alignItems: 'center',
801
+ paddingHorizontal: 16,
802
+ paddingVertical: 10,
803
+ },
804
+ albumSelector: { flexDirection: 'row', alignItems: 'center' },
805
+ albumTitle: { fontSize: 16, fontWeight: '700', color: '#fff' },
806
+ albumArrow: {
807
+ width: 10,
808
+ height: 10,
809
+ borderRightWidth: 2,
810
+ borderBottomWidth: 2,
811
+ borderColor: '#fff',
812
+ marginLeft: 8,
813
+ marginTop: -4,
814
+ transform: [{ rotate: '45deg' }],
815
+ },
816
+ multiSelectBtn: {
817
+ backgroundColor: '#111827',
818
+ paddingHorizontal: 16,
819
+ paddingVertical: 8,
820
+ borderRadius: 18,
821
+ },
822
+ cameraGridContainer: {
823
+ width: '25%',
824
+ aspectRatio: 1,
825
+ padding: 1,
826
+ },
827
+ cameraGridBox: {
828
+ flex: 1,
829
+ backgroundColor: '#262626',
830
+ borderRadius: 8,
831
+ alignItems: 'center',
832
+ justifyContent: 'center',
833
+ },
834
+ multiSelectBtnActive: { backgroundColor: '#2563eb' },
835
+ multiSelectText: { color: '#e5e7eb', fontSize: 12, fontWeight: '700' },
836
+ multiSelectTextActive: { color: '#fff' },
837
+ postTypeBar: {
838
+ flexDirection: 'row',
839
+ justifyContent: 'space-evenly',
840
+ paddingVertical: 12,
841
+ backgroundColor: '#0b0f1a',
842
+ },
843
+ postTypeChip: {
844
+ paddingHorizontal: 28,
845
+ paddingVertical: 10,
846
+ borderRadius: 20,
847
+ backgroundColor: '#111827',
848
+ },
849
+ postTypeChipActive: { backgroundColor: '#1d4ed8' },
850
+ postTypeText: { color: '#9ca3af', fontWeight: '700', letterSpacing: 0.4 },
851
+ postTypeTextActive: { color: '#fff' },
852
+ grid: { paddingBottom: 80 },
853
+ libraryList: { flex: 1 },
854
+ thumbContainer: { width: '25%', aspectRatio: 1, padding: 1 },
855
+ thumb: { width: '100%', height: '100%', borderRadius: 8 },
856
+ thumbFallback: { backgroundColor: '#111' },
857
+ activeOverlay: {
858
+ ...StyleSheet.absoluteFillObject,
859
+ backgroundColor: 'rgba(255,255,255,0.18)',
860
+ },
861
+ multiOverlay: {
862
+ position: 'absolute',
863
+ right: 6,
864
+ top: 6,
865
+ },
866
+ multiOverlayActive: {},
867
+ selectionBadge: {
868
+ width: 22,
869
+ height: 22,
870
+ borderRadius: 11,
871
+ backgroundColor: '#2563eb',
872
+ alignItems: 'center',
873
+ justifyContent: 'center',
874
+ borderWidth: 1.5,
875
+ borderColor: '#fff',
876
+ },
877
+ selectionNumber: { color: '#fff', fontSize: 11, fontWeight: '700' },
878
+ emptyBadge: {
879
+ width: 22,
880
+ height: 22,
881
+ borderRadius: 11,
882
+ borderWidth: 1.5,
883
+ borderColor: '#fff',
884
+ backgroundColor: 'rgba(0,0,0,0.25)',
885
+ },
886
+ videoBadge: {
887
+ position: 'absolute',
888
+ right: 6,
889
+ bottom: 6,
890
+ backgroundColor: 'rgba(0,0,0,0.6)',
891
+ paddingHorizontal: 6,
892
+ paddingVertical: 2,
893
+ borderRadius: 6,
894
+ },
895
+ videoDuration: { color: '#fff', fontSize: 10, fontWeight: '600' },
896
+ videoPlayBadge: {
897
+ position: 'absolute',
898
+ left: 6,
899
+ top: 6,
900
+ width: 20,
901
+ height: 20,
902
+ borderRadius: 10,
903
+ backgroundColor: 'rgba(0,0,0,0.6)',
904
+ alignItems: 'center',
905
+ justifyContent: 'center',
906
+ },
907
+ videoPlayIcon: { color: '#fff', fontSize: 10, fontWeight: '700' },
908
+ previewOverlay: {
909
+ position: 'absolute',
910
+ left: 0,
911
+ right: 0,
912
+ bottom: 14,
913
+ alignItems: 'center',
914
+ },
915
+ playPauseCircle: {
916
+ width: 44,
917
+ height: 44,
918
+ borderRadius: 22,
919
+ backgroundColor: 'rgba(0,0,0,0.55)',
920
+ alignItems: 'center',
921
+ justifyContent: 'center',
922
+ },
923
+ playPauseText: { color: '#fff', fontSize: 18, fontWeight: '700' },
924
+ empty: { padding: 24, alignItems: 'center' },
925
+ emptyText: { color: '#6b7280' },
926
+ tabBar: {
927
+ flexDirection: 'row',
928
+ justifyContent: 'space-around',
929
+ paddingVertical: 10,
930
+ borderTopWidth: 1,
931
+ borderTopColor: '#111827',
932
+ },
933
+ tab: { color: '#6b7280', fontWeight: '700' },
934
+ tabActive: { color: '#fff' },
935
+ cameraContainer: {
936
+ flex: 1,
937
+ backgroundColor: '#000',
938
+ },
939
+ cameraGridOverlay: {
940
+ ...StyleSheet.absoluteFillObject,
941
+ justifyContent: 'space-between',
942
+ paddingVertical: 120,
943
+ },
944
+ gridRow: {
945
+ flex: 1,
946
+ flexDirection: 'row',
947
+ },
948
+ gridRowMiddleRow: {
949
+ borderTopWidth: 1,
950
+ borderBottomWidth: 1,
951
+ borderColor: 'rgba(255, 255, 255, 0.15)',
952
+ },
953
+ gridCell: {
954
+ flex: 1,
955
+ },
956
+ gridCellMiddleCol: {
957
+ borderLeftWidth: 1,
958
+ borderRightWidth: 1,
959
+ borderColor: 'rgba(255, 255, 255, 0.15)',
960
+ },
961
+ cameraHeader: {
962
+ position: 'absolute',
963
+ top: 40,
964
+ left: 0,
965
+ right: 0,
966
+ flexDirection: 'row',
967
+ justifyContent: 'space-between',
968
+ alignItems: 'center',
969
+ paddingHorizontal: 20,
970
+ zIndex: 10,
971
+ },
972
+ cameraCloseBtn: {
973
+ width: 40,
974
+ height: 40,
975
+ borderRadius: 20,
976
+ backgroundColor: 'rgba(0,0,0,0.3)',
977
+ alignItems: 'center',
978
+ justifyContent: 'center',
979
+ },
980
+ cameraCloseText: {
981
+ color: '#fff',
982
+ fontSize: 20,
983
+ fontWeight: '300',
984
+ },
985
+ cameraFlashContainer: {
986
+ width: 40,
987
+ height: 40,
988
+ borderRadius: 20,
989
+ backgroundColor: 'rgba(0,0,0,0.3)',
990
+ alignItems: 'center',
991
+ justifyContent: 'center',
992
+ },
993
+ cameraHeaderText: {
994
+ color: '#fff',
995
+ fontSize: 20,
996
+ },
997
+ cameraHeaderActiveText: {
998
+ color: '#FFD700',
999
+ },
1000
+ cameraFooter: {
1001
+ position: 'absolute',
1002
+ bottom: 80,
1003
+ left: 0,
1004
+ right: 0,
1005
+ flexDirection: 'row',
1006
+ justifyContent: 'space-between',
1007
+ alignItems: 'center',
1008
+ paddingHorizontal: 40,
1009
+ zIndex: 10,
1010
+ },
1011
+ cameraGalleryPreview: {
1012
+ width: 44,
1013
+ height: 44,
1014
+ borderRadius: 8,
1015
+ borderWidth: 2,
1016
+ borderColor: '#fff',
1017
+ overflow: 'hidden',
1018
+ backgroundColor: '#333',
1019
+ },
1020
+ cameraGalleryThumb: {
1021
+ width: '100%',
1022
+ height: '100%',
1023
+ },
1024
+ cameraGalleryThumbPlaceholder: {
1025
+ width: '100%',
1026
+ height: '100%',
1027
+ backgroundColor: '#333',
1028
+ },
1029
+ captureRing: {
1030
+ width: 84,
1031
+ height: 84,
1032
+ borderRadius: 42,
1033
+ borderWidth: 4,
1034
+ borderColor: '#fff',
1035
+ alignItems: 'center',
1036
+ justifyContent: 'center',
1037
+ backgroundColor: 'transparent',
1038
+ },
1039
+ captureRingRecording: {
1040
+ borderColor: 'rgba(255, 0, 0, 0.4)',
1041
+ },
1042
+ captureButton: {
1043
+ width: 66,
1044
+ height: 66,
1045
+ borderRadius: 33,
1046
+ backgroundColor: '#fff',
1047
+ },
1048
+ captureButtonRecording: {
1049
+ backgroundColor: '#ef4444',
1050
+ borderRadius: 8,
1051
+ width: 32,
1052
+ height: 32,
1053
+ },
1054
+ flipCameraBtn: {
1055
+ width: 44,
1056
+ height: 44,
1057
+ borderRadius: 22,
1058
+ backgroundColor: 'rgba(0,0,0,0.3)',
1059
+ alignItems: 'center',
1060
+ justifyContent: 'center',
1061
+ },
1062
+ flipCameraText: {
1063
+ color: '#fff',
1064
+ fontSize: 22,
1065
+ },
1066
+ cameraModeBar: {
1067
+ position: 'absolute',
1068
+ bottom: 30,
1069
+ left: 0,
1070
+ right: 0,
1071
+ flexDirection: 'row',
1072
+ zIndex: 10,
1073
+ },
1074
+ cameraModeScrollContainer: {
1075
+ alignItems: 'center',
1076
+ justifyContent: 'center',
1077
+ minWidth: '100%',
1078
+ paddingHorizontal: 20,
1079
+ },
1080
+ cameraModeTextActive: {
1081
+ color: '#fff',
1082
+ fontSize: 13,
1083
+ fontWeight: 'bold',
1084
+ marginHorizontal: 15,
1085
+ letterSpacing: 1.5,
1086
+ },
1087
+ cameraModeTextInactive: {
1088
+ color: 'rgba(255, 255, 255, 0.5)',
1089
+ fontSize: 13,
1090
+ fontWeight: '600',
1091
+ marginHorizontal: 15,
1092
+ letterSpacing: 1.5,
1093
+ },
1094
+ });