@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,282 @@
1
+ import React, { useState, useMemo } from 'react';
2
+ import {
3
+ View,
4
+ Text,
5
+ StyleSheet,
6
+ FlatList,
7
+ Image,
8
+ Pressable,
9
+ Dimensions,
10
+ Platform,
11
+ } from 'react-native';
12
+ import { SafeAreaView } from 'react-native-safe-area-context';
13
+ import { VideoPreview } from '../native/VideoPreview';
14
+ import type { MediaItem } from '../types';
15
+
16
+ const { width: SCREEN_WIDTH } = Dimensions.get('window');
17
+
18
+ // Custom Home Icon
19
+ const HomeIcon = () => (
20
+ <View style={iconStyles.homeContainer}>
21
+ <View style={iconStyles.homeRoof} />
22
+ <View style={iconStyles.homeBase} />
23
+ </View>
24
+ );
25
+
26
+ const iconStyles = StyleSheet.create({
27
+ homeContainer: { width: 24, height: 24, alignItems: 'center', justifyContent: 'center' },
28
+ homeRoof: {
29
+ width: 0,
30
+ height: 0,
31
+ borderLeftWidth: 10,
32
+ borderRightWidth: 10,
33
+ borderBottomWidth: 10,
34
+ borderStyle: 'solid',
35
+ backgroundColor: 'transparent',
36
+ borderLeftColor: 'transparent',
37
+ borderRightColor: 'transparent',
38
+ borderBottomColor: '#fff',
39
+ marginBottom: -2
40
+ },
41
+ homeBase: { width: 16, height: 10, backgroundColor: '#fff', borderRadius: 1 },
42
+ });
43
+
44
+ interface ExportScreenProps {
45
+ editedMedia: Record<string, MediaItem>;
46
+ onHome: () => void;
47
+ onReEdit: (item: MediaItem) => void;
48
+ }
49
+
50
+ export function ExportScreen({ editedMedia, onHome, onReEdit }: ExportScreenProps) {
51
+ const mediaList = useMemo(() => Object.values(editedMedia), [editedMedia]);
52
+ const [selectedId, setSelectedId] = useState<string>(mediaList[0]?.id || '');
53
+
54
+ const selectedItem = useMemo(() =>
55
+ mediaList.find(m => m.id === selectedId) || mediaList[0],
56
+ [mediaList, selectedId]
57
+ );
58
+
59
+ const renderItem = ({ item }: { item: MediaItem }) => {
60
+ const isSelected = item.id === selectedId;
61
+ return (
62
+ <Pressable
63
+ style={[styles.thumbContainer, isSelected && styles.activeThumb]}
64
+ onPress={() => setSelectedId(item.id)}
65
+ >
66
+ <Image
67
+ source={{ uri: item.thumbnailUri || item.uri }}
68
+ style={styles.thumb}
69
+ resizeMode="cover"
70
+ />
71
+ {item.type === 'video' && (
72
+ <View style={styles.videoBadge}>
73
+ <Text style={styles.videoBadgeText}>▶</Text>
74
+ </View>
75
+ )}
76
+ </Pressable>
77
+ );
78
+ };
79
+
80
+ return (
81
+ <View style={styles.container}>
82
+ <View style={styles.safeArea}>
83
+ {/* Header */}
84
+ <View style={styles.header}>
85
+ <Pressable onPress={onHome} style={styles.headerBtn}>
86
+ <HomeIcon />
87
+ </Pressable>
88
+ <Text style={styles.headerTitle}>Exported Media</Text>
89
+ <View style={{ width: 44 }} />
90
+ </View>
91
+
92
+ {/* Main Preview */}
93
+ <View style={styles.previewSection}>
94
+ {selectedItem ? (
95
+ <View style={styles.mainPreviewWrapper}>
96
+ {selectedItem.type === 'video' ? (
97
+ <VideoPreview
98
+ uri={selectedItem.uri}
99
+ paused={false}
100
+ muted={false}
101
+ style={styles.mainPreview}
102
+ />
103
+ ) : (
104
+ <Image
105
+ source={{ uri: selectedItem.uri }}
106
+ style={styles.mainPreview}
107
+ resizeMode="contain"
108
+ />
109
+ )}
110
+ </View>
111
+ ) : (
112
+ <View style={styles.emptyView}>
113
+ <Text style={styles.emptyText}>No exported media found.</Text>
114
+ </View>
115
+ )}
116
+ </View>
117
+
118
+ {/* List of Edited items */}
119
+ <View style={styles.listSection}>
120
+ <Text style={styles.listTitle}>All Edited Items ({mediaList.length})</Text>
121
+ <FlatList
122
+ data={mediaList}
123
+ renderItem={renderItem}
124
+ keyExtractor={item => item.id}
125
+ horizontal
126
+ showsHorizontalScrollIndicator={false}
127
+ contentContainerStyle={styles.listContent}
128
+ />
129
+ </View>
130
+
131
+ {/* Bottom Actions */}
132
+ <View style={styles.footer}>
133
+ <Pressable
134
+ style={styles.reEditBtn}
135
+ onPress={() => selectedItem && onReEdit(selectedItem)}
136
+ >
137
+ <Text style={styles.reEditText}>Edit Again</Text>
138
+ </Pressable>
139
+
140
+ <Pressable style={styles.shareBtn} onPress={() => {}}>
141
+ <Text style={styles.shareText}>Share Final</Text>
142
+ </Pressable>
143
+ </View>
144
+ </View>
145
+ </View>
146
+ );
147
+ }
148
+
149
+ const styles = StyleSheet.create({
150
+ container: {
151
+ flex: 1,
152
+ backgroundColor: '#0b0f1a',
153
+ },
154
+ safeArea: {
155
+ flex: 1,
156
+ },
157
+ header: {
158
+ flexDirection: 'row',
159
+ alignItems: 'center',
160
+ justifyContent: 'space-between',
161
+ paddingHorizontal: 20,
162
+ height: 60,
163
+ borderBottomWidth: 1,
164
+ borderBottomColor: '#1e293b',
165
+ },
166
+ headerBtn: {
167
+ width: 44,
168
+ height: 44,
169
+ alignItems: 'center',
170
+ justifyContent: 'center',
171
+ },
172
+ headerTitle: {
173
+ color: '#fff',
174
+ fontSize: 18,
175
+ fontWeight: '700',
176
+ },
177
+ previewSection: {
178
+ flex: 1,
179
+ padding: 20,
180
+ justifyContent: 'center',
181
+ },
182
+ mainPreviewWrapper: {
183
+ flex: 1,
184
+ borderRadius: 16,
185
+ overflow: 'hidden',
186
+ backgroundColor: '#000',
187
+ elevation: 10,
188
+ shadowColor: '#000',
189
+ shadowOffset: { width: 0, height: 5 },
190
+ shadowOpacity: 0.3,
191
+ shadowRadius: 10,
192
+ },
193
+ mainPreview: {
194
+ flex: 1,
195
+ },
196
+ listSection: {
197
+ paddingVertical: 20,
198
+ },
199
+ listTitle: {
200
+ color: '#94a3b8',
201
+ fontSize: 13,
202
+ fontWeight: '600',
203
+ paddingHorizontal: 20,
204
+ marginBottom: 12,
205
+ textTransform: 'uppercase',
206
+ },
207
+ listContent: {
208
+ paddingHorizontal: 16,
209
+ },
210
+ thumbContainer: {
211
+ width: 80,
212
+ height: 80,
213
+ borderRadius: 12,
214
+ marginHorizontal: 4,
215
+ borderWidth: 2,
216
+ borderColor: 'transparent',
217
+ overflow: 'hidden',
218
+ backgroundColor: '#1e293b',
219
+ },
220
+ activeThumb: {
221
+ borderColor: '#3b82f6',
222
+ },
223
+ thumb: {
224
+ width: '100%',
225
+ height: '100%',
226
+ },
227
+ videoBadge: {
228
+ position: 'absolute',
229
+ top: 4,
230
+ right: 4,
231
+ backgroundColor: 'rgba(0,0,0,0.5)',
232
+ width: 16,
233
+ height: 16,
234
+ borderRadius: 8,
235
+ alignItems: 'center',
236
+ justifyContent: 'center',
237
+ },
238
+ videoBadgeText: {
239
+ color: '#fff',
240
+ fontSize: 8,
241
+ },
242
+ footer: {
243
+ flexDirection: 'row',
244
+ padding: 20,
245
+ gap: 12,
246
+ },
247
+ reEditBtn: {
248
+ flex: 1,
249
+ backgroundColor: '#1e293b',
250
+ paddingVertical: 16,
251
+ borderRadius: 14,
252
+ alignItems: 'center',
253
+ borderWidth: 1,
254
+ borderColor: '#334155',
255
+ },
256
+ reEditText: {
257
+ color: '#fff',
258
+ fontSize: 16,
259
+ fontWeight: '600',
260
+ },
261
+ shareBtn: {
262
+ flex: 1,
263
+ backgroundColor: '#3b82f6',
264
+ paddingVertical: 16,
265
+ borderRadius: 14,
266
+ alignItems: 'center',
267
+ },
268
+ shareText: {
269
+ color: '#fff',
270
+ fontSize: 16,
271
+ fontWeight: '700',
272
+ },
273
+ emptyView: {
274
+ flex: 1,
275
+ alignItems: 'center',
276
+ justifyContent: 'center',
277
+ },
278
+ emptyText: {
279
+ color: '#64748b',
280
+ fontSize: 16,
281
+ },
282
+ });
@@ -0,0 +1,412 @@
1
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
2
+ import {
3
+ Alert,
4
+ Animated,
5
+ FlatList,
6
+ Image,
7
+ Pressable,
8
+ StyleSheet,
9
+ Text,
10
+ View,
11
+ } from 'react-native';
12
+ import { exportAsset } from '../native/MediaLibrary';
13
+ import { VideoPreview } from '../native/VideoPreview';
14
+ import type { MediaItem } from '../types';
15
+
16
+ export function GalleryScreen({
17
+ items,
18
+ onBack,
19
+ onSelect,
20
+ onNext,
21
+ }: {
22
+ items: MediaItem[];
23
+ onBack: () => void;
24
+ onSelect: (item: MediaItem) => void;
25
+ onNext: () => void;
26
+ }) {
27
+ const [selectedMedia, setSelectedMedia] = useState<MediaItem | null>(items[0] || null);
28
+ const [previewUri, setPreviewUri] = useState<string | null>(null);
29
+ const [cropMode, setCropMode] = useState<'1:1' | 'original'>('1:1');
30
+ const [videoPaused, setVideoPaused] = useState(false);
31
+ const scaleAnim = useRef(new Animated.Value(1)).current;
32
+
33
+ // Sync selected media if items list changes
34
+ useEffect(() => {
35
+ if (items.length > 0) {
36
+ const found = selectedMedia ? items.find((i) => i.id === selectedMedia.id) : null;
37
+ if (found) {
38
+ setSelectedMedia(found);
39
+ } else {
40
+ setSelectedMedia(items[0]);
41
+ }
42
+ } else {
43
+ setSelectedMedia(null);
44
+ }
45
+ }, [items]);
46
+
47
+ // Resolve ph:// asset URIs to cache paths
48
+ useEffect(() => {
49
+ let cancelled = false;
50
+ (async () => {
51
+ if (!selectedMedia) {
52
+ setPreviewUri(null);
53
+ return;
54
+ }
55
+ if (!selectedMedia.uri.startsWith('ph://') && !selectedMedia.uri.startsWith('content://')) {
56
+ setPreviewUri(selectedMedia.uri);
57
+ return;
58
+ }
59
+ try {
60
+ const fileUri = await exportAsset(selectedMedia.id);
61
+ if (!cancelled) setPreviewUri(fileUri);
62
+ } catch {
63
+ if (!cancelled) setPreviewUri(null);
64
+ }
65
+ })();
66
+ return () => {
67
+ cancelled = true;
68
+ };
69
+ }, [selectedMedia]);
70
+
71
+ const playableUri = useMemo(() => {
72
+ if (!selectedMedia) return null;
73
+ if (previewUri && !previewUri.startsWith('ph://') && !previewUri.startsWith('content://')) return previewUri;
74
+ if (selectedMedia.uri && !selectedMedia.uri.startsWith('ph://') && !selectedMedia.uri.startsWith('content://')) return selectedMedia.uri;
75
+ return null;
76
+ }, [previewUri, selectedMedia]);
77
+
78
+ const handleSelectItem = (item: MediaItem) => {
79
+ if (selectedMedia?.id === item.id) {
80
+ // Tap again on active item to edit it directly
81
+ onSelect(item);
82
+ } else {
83
+ setSelectedMedia(item);
84
+ setVideoPaused(false);
85
+ Animated.sequence([
86
+ Animated.timing(scaleAnim, { toValue: 0.97, duration: 80, useNativeDriver: true }),
87
+ Animated.timing(scaleAnim, { toValue: 1, duration: 80, useNativeDriver: true }),
88
+ ]).start();
89
+ }
90
+ };
91
+
92
+ const getSelectionIndex = (id: string) => {
93
+ const idx = items.findIndex((i) => i.id === id);
94
+ return idx === -1 ? null : idx + 1;
95
+ };
96
+
97
+ const renderThumb = ({ item }: { item: MediaItem }) => {
98
+ const thumbUri = item.thumbnailUri ?? item.uri;
99
+ const isActive = selectedMedia?.id === item.id;
100
+ const itemIndex = getSelectionIndex(item.id);
101
+
102
+ return (
103
+ <Pressable style={styles.thumbContainer} onPress={() => handleSelectItem(item)}>
104
+ {thumbUri && !thumbUri.startsWith('ph://') ? (
105
+ <Image source={{ uri: thumbUri }} style={styles.thumb} />
106
+ ) : (
107
+ <View style={[styles.thumb, styles.thumbFallback]} />
108
+ )}
109
+
110
+ {isActive && <View style={styles.activeOverlay} />}
111
+
112
+ {itemIndex !== null && (
113
+ <View style={styles.multiOverlay}>
114
+ <View style={[styles.selectionBadge, isActive && styles.selectionBadgeActive]}>
115
+ <Text style={styles.selectionNumber}>{itemIndex}</Text>
116
+ </View>
117
+ </View>
118
+ )}
119
+
120
+ {item.type === 'video' && (
121
+ <>
122
+ <View style={styles.videoPlayBadge}>
123
+ <Text style={styles.videoPlayIcon}>▶</Text>
124
+ </View>
125
+ <View style={styles.videoBadge}>
126
+ <Text style={styles.videoDuration}>
127
+ {item.durationMs ? `${(item.durationMs / 1000).toFixed(0)}s` : 'video'}
128
+ </Text>
129
+ </View>
130
+ </>
131
+ )}
132
+
133
+ <View style={styles.editLabelContainer}>
134
+ <Text style={styles.editLabelText}>EDIT</Text>
135
+ </View>
136
+ </Pressable>
137
+ );
138
+ };
139
+
140
+ return (
141
+ <View style={styles.container}>
142
+ {/* Header */}
143
+ <View style={styles.header}>
144
+ <Pressable onPress={onBack} style={styles.backButton}>
145
+ <Text style={styles.headerCancel}>✕</Text>
146
+ </Pressable>
147
+
148
+ <Text style={styles.headerTitle}>All Selected</Text>
149
+
150
+ <Pressable style={styles.nextBtn} onPress={onNext}>
151
+ <Text style={styles.nextText}>Next</Text>
152
+ </Pressable>
153
+ </View>
154
+
155
+ {/* Main Preview Area */}
156
+ <Animated.View style={[styles.preview, { transform: [{ scale: scaleAnim }] }]}>
157
+ {selectedMedia ? (
158
+ selectedMedia.type === 'video' ? (
159
+ <Pressable style={styles.previewImage} onPress={() => setVideoPaused((v) => !v)}>
160
+ {playableUri ? (
161
+ <VideoPreview uri={playableUri} paused={videoPaused} muted={false} style={styles.previewImage} />
162
+ ) : (
163
+ <Image
164
+ source={{ uri: selectedMedia.thumbnailUri || selectedMedia.uri }}
165
+ style={styles.previewImage}
166
+ resizeMode="cover"
167
+ />
168
+ )}
169
+ <View style={styles.previewOverlay}>
170
+ <View style={styles.playPauseCircle}>
171
+ <Text style={styles.playPauseText}>{videoPaused ? '▶' : '❚❚'}</Text>
172
+ </View>
173
+ </View>
174
+ </Pressable>
175
+ ) : (
176
+ <Pressable style={styles.previewImage} onPress={() => onSelect(selectedMedia)}>
177
+ <Image
178
+ source={{ uri: previewUri ?? selectedMedia.uri }}
179
+ style={[styles.previewImage, cropMode === '1:1' ? styles.squareCrop : styles.originalCrop]}
180
+ resizeMode={cropMode === '1:1' ? 'cover' : 'contain'}
181
+ />
182
+ </Pressable>
183
+ )
184
+ ) : (
185
+ <View style={styles.emptyPreview}>
186
+ <Text style={styles.emptyText}>No Media Selected</Text>
187
+ </View>
188
+ )}
189
+
190
+ {selectedMedia && (
191
+ <>
192
+ {/* Crop Toggle (for images) */}
193
+ {selectedMedia.type === 'image' && (
194
+ <View style={styles.previewControls}>
195
+ <Pressable
196
+ style={styles.cropToggle}
197
+ onPress={() => setCropMode((v) => (v === '1:1' ? 'original' : '1:1'))}
198
+ >
199
+ <Text style={styles.cropIcon}>{cropMode === '1:1' ? '⊡' : '⊞'}</Text>
200
+ </Pressable>
201
+ </View>
202
+ )}
203
+
204
+ {/* Floating Edit Badge */}
205
+ <Pressable
206
+ style={styles.editBadge}
207
+ onPress={() => onSelect(selectedMedia)}
208
+ >
209
+ <Text style={styles.editBadgeText}>🎨 Edit Media</Text>
210
+ </Pressable>
211
+ </>
212
+ )}
213
+ </Animated.View>
214
+
215
+ {/* Row details */}
216
+ <View style={styles.albumRow}>
217
+ <View style={styles.albumSelector}>
218
+ <Text style={styles.albumTitle}>Selected Items</Text>
219
+ </View>
220
+ {selectedMedia && (
221
+ <View style={styles.itemCountBadge}>
222
+ <Text style={styles.itemCountText}>
223
+ {items.indexOf(selectedMedia) + 1} of {items.length}
224
+ </Text>
225
+ </View>
226
+ )}
227
+ </View>
228
+
229
+ {/* Grid of Selected Items */}
230
+ <FlatList
231
+ data={items}
232
+ keyExtractor={(item) => `${item.id}-${item.uri}`}
233
+ numColumns={4}
234
+ style={styles.libraryList}
235
+ renderItem={renderThumb}
236
+ contentContainerStyle={styles.grid}
237
+ ListEmptyComponent={
238
+ <View style={styles.empty}>
239
+ <Text style={styles.emptyText}>No items picked yet.</Text>
240
+ </View>
241
+ }
242
+ />
243
+ </View>
244
+ );
245
+ }
246
+
247
+ const styles = StyleSheet.create({
248
+ container: { flex: 1, backgroundColor: '#0b0f1a' },
249
+ header: {
250
+ flexDirection: 'row',
251
+ justifyContent: 'space-between',
252
+ alignItems: 'center',
253
+ paddingHorizontal: 16,
254
+ paddingVertical: 12,
255
+ borderBottomWidth: 0,
256
+ backgroundColor: '#0b0f1a',
257
+ zIndex: 10,
258
+ elevation: 10,
259
+ },
260
+ headerTitle: { color: '#fff', fontWeight: '700', fontSize: 18 },
261
+ headerCancel: { fontSize: 20, color: '#fff' },
262
+ backButton: {
263
+ paddingVertical: 4,
264
+ paddingHorizontal: 8,
265
+ },
266
+ nextBtn: {
267
+ backgroundColor: '#2563eb',
268
+ paddingHorizontal: 14,
269
+ paddingVertical: 8,
270
+ borderRadius: 20,
271
+ },
272
+ nextText: { color: '#fff', fontWeight: '700' },
273
+ preview: { width: '100%', height: 420, backgroundColor: '#0f172a' },
274
+ previewImage: { width: '100%', height: '100%' },
275
+ squareCrop: { height: '100%' },
276
+ originalCrop: { height: '100%' },
277
+ previewControls: {
278
+ position: 'absolute',
279
+ left: 16,
280
+ bottom: 16,
281
+ flexDirection: 'row',
282
+ alignItems: 'center',
283
+ },
284
+ cropToggle: {
285
+ backgroundColor: 'rgba(0,0,0,0.65)',
286
+ width: 34,
287
+ height: 34,
288
+ borderRadius: 17,
289
+ alignItems: 'center',
290
+ justifyContent: 'center',
291
+ },
292
+ cropIcon: { color: '#fff', fontSize: 16 },
293
+ editBadge: {
294
+ position: 'absolute',
295
+ right: 16,
296
+ bottom: 16,
297
+ backgroundColor: '#2563eb',
298
+ paddingHorizontal: 14,
299
+ paddingVertical: 8,
300
+ borderRadius: 20,
301
+ flexDirection: 'row',
302
+ alignItems: 'center',
303
+ shadowColor: '#000',
304
+ shadowOffset: { width: 0, height: 2 },
305
+ shadowOpacity: 0.35,
306
+ shadowRadius: 4,
307
+ elevation: 6,
308
+ },
309
+ editBadgeText: {
310
+ color: '#fff',
311
+ fontWeight: '700',
312
+ fontSize: 13,
313
+ },
314
+ albumRow: {
315
+ flexDirection: 'row',
316
+ justifyContent: 'space-between',
317
+ alignItems: 'center',
318
+ paddingHorizontal: 16,
319
+ paddingVertical: 12,
320
+ },
321
+ albumSelector: { flexDirection: 'row', alignItems: 'center' },
322
+ albumTitle: { fontSize: 16, fontWeight: '700', color: '#fff' },
323
+ itemCountBadge: {
324
+ backgroundColor: '#111827',
325
+ paddingHorizontal: 12,
326
+ paddingVertical: 6,
327
+ borderRadius: 12,
328
+ },
329
+ itemCountText: { color: '#e5e7eb', fontSize: 12, fontWeight: '700' },
330
+ grid: { paddingBottom: 40 },
331
+ libraryList: { flex: 1 },
332
+ thumbContainer: { width: '25%', aspectRatio: 1, padding: 1 },
333
+ thumb: { width: '100%', height: '100%', borderRadius: 8 },
334
+ thumbFallback: { backgroundColor: '#111' },
335
+ activeOverlay: {
336
+ ...StyleSheet.absoluteFillObject,
337
+ borderRadius: 8,
338
+ borderWidth: 2,
339
+ borderColor: '#2563eb',
340
+ backgroundColor: 'rgba(37,99,235,0.12)',
341
+ },
342
+ multiOverlay: {
343
+ position: 'absolute',
344
+ right: 6,
345
+ top: 6,
346
+ },
347
+ selectionBadge: {
348
+ width: 20,
349
+ height: 20,
350
+ borderRadius: 10,
351
+ backgroundColor: 'rgba(0,0,0,0.5)',
352
+ alignItems: 'center',
353
+ justifyContent: 'center',
354
+ borderWidth: 1.5,
355
+ borderColor: '#fff',
356
+ },
357
+ selectionBadgeActive: {
358
+ backgroundColor: '#2563eb',
359
+ },
360
+ selectionNumber: { color: '#fff', fontSize: 10, fontWeight: '700' },
361
+ videoBadge: {
362
+ position: 'absolute',
363
+ right: 6,
364
+ bottom: 6,
365
+ backgroundColor: 'rgba(0,0,0,0.6)',
366
+ paddingHorizontal: 6,
367
+ paddingVertical: 2,
368
+ borderRadius: 6,
369
+ },
370
+ videoDuration: { color: '#fff', fontSize: 9, fontWeight: '600' },
371
+ videoPlayBadge: {
372
+ position: 'absolute',
373
+ left: 6,
374
+ top: 6,
375
+ width: 18,
376
+ height: 18,
377
+ borderRadius: 9,
378
+ backgroundColor: 'rgba(0,0,0,0.6)',
379
+ alignItems: 'center',
380
+ justifyContent: 'center',
381
+ },
382
+ videoPlayIcon: { color: '#fff', fontSize: 9, fontWeight: '700' },
383
+ previewOverlay: {
384
+ position: 'absolute',
385
+ left: 0,
386
+ right: 0,
387
+ bottom: 14,
388
+ alignItems: 'center',
389
+ },
390
+ playPauseCircle: {
391
+ width: 44,
392
+ height: 44,
393
+ borderRadius: 22,
394
+ backgroundColor: 'rgba(0,0,0,0.55)',
395
+ alignItems: 'center',
396
+ justifyContent: 'center',
397
+ },
398
+ playPauseText: { color: '#fff', fontSize: 18, fontWeight: '700' },
399
+ editLabelContainer: {
400
+ position: 'absolute',
401
+ bottom: 6,
402
+ left: 6,
403
+ backgroundColor: 'rgba(0,0,0,0.5)',
404
+ paddingHorizontal: 6,
405
+ paddingVertical: 2,
406
+ borderRadius: 4,
407
+ },
408
+ editLabelText: { color: '#fff', fontSize: 9, fontWeight: '700' },
409
+ empty: { padding: 24, alignItems: 'center' },
410
+ emptyText: { color: '#6b7280' },
411
+ emptyPreview: { flex: 1, alignItems: 'center', justifyContent: 'center' },
412
+ });