react-native-picture-selector 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.
Files changed (99) hide show
  1. package/README.md +627 -0
  2. package/android/CMakeLists.txt +30 -0
  3. package/android/build.gradle +79 -0
  4. package/android/proguard-rules.pro +21 -0
  5. package/android/src/main/AndroidManifest.xml +39 -0
  6. package/android/src/main/kotlin/com/margelo/pictureselector/GlideEngine.kt +80 -0
  7. package/android/src/main/kotlin/com/margelo/pictureselector/HybridPictureSelector.kt +138 -0
  8. package/android/src/main/kotlin/com/margelo/pictureselector/LubanCompressEngine.kt +58 -0
  9. package/android/src/main/kotlin/com/margelo/pictureselector/MediaAssetMapper.kt +69 -0
  10. package/android/src/main/kotlin/com/margelo/pictureselector/NitroPictureSelectorPackage.kt +52 -0
  11. package/android/src/main/kotlin/com/margelo/pictureselector/PictureSelectorOptionsMapper.kt +105 -0
  12. package/android/src/main/kotlin/com/margelo/pictureselector/UCropEngine.kt +57 -0
  13. package/android/src/main/res/xml/file_paths.xml +8 -0
  14. package/ios/HybridPictureSelector.swift +386 -0
  15. package/ios/NitroPictureSelector.podspec +39 -0
  16. package/lib/commonjs/PictureSelector.js +74 -0
  17. package/lib/commonjs/PictureSelector.js.map +1 -0
  18. package/lib/commonjs/index.js +39 -0
  19. package/lib/commonjs/index.js.map +1 -0
  20. package/lib/commonjs/package.json +1 -0
  21. package/lib/commonjs/specs/PictureSelector.nitro.js +34 -0
  22. package/lib/commonjs/specs/PictureSelector.nitro.js.map +1 -0
  23. package/lib/commonjs/types.js +44 -0
  24. package/lib/commonjs/types.js.map +1 -0
  25. package/lib/commonjs/usePictureSelector.js +122 -0
  26. package/lib/commonjs/usePictureSelector.js.map +1 -0
  27. package/lib/module/PictureSelector.js +71 -0
  28. package/lib/module/PictureSelector.js.map +1 -0
  29. package/lib/module/index.js +6 -0
  30. package/lib/module/index.js.map +1 -0
  31. package/lib/module/package.json +1 -0
  32. package/lib/module/specs/PictureSelector.nitro.js +36 -0
  33. package/lib/module/specs/PictureSelector.nitro.js.map +1 -0
  34. package/lib/module/types.js +29 -0
  35. package/lib/module/types.js.map +1 -0
  36. package/lib/module/usePictureSelector.js +119 -0
  37. package/lib/module/usePictureSelector.js.map +1 -0
  38. package/lib/typescript/PictureSelector.d.ts +23 -0
  39. package/lib/typescript/PictureSelector.d.ts.map +1 -0
  40. package/lib/typescript/index.d.ts +6 -0
  41. package/lib/typescript/index.d.ts.map +1 -0
  42. package/lib/typescript/specs/PictureSelector.nitro.d.ts +96 -0
  43. package/lib/typescript/specs/PictureSelector.nitro.d.ts.map +1 -0
  44. package/lib/typescript/types.d.ts +16 -0
  45. package/lib/typescript/types.d.ts.map +1 -0
  46. package/lib/typescript/usePictureSelector.d.ts +26 -0
  47. package/lib/typescript/usePictureSelector.d.ts.map +1 -0
  48. package/nitro.json +11 -0
  49. package/nitrogen/generated/.gitattributes +1 -0
  50. package/nitrogen/generated/android/NitroPictureSelector+autolinking.cmake +81 -0
  51. package/nitrogen/generated/android/NitroPictureSelector+autolinking.gradle +27 -0
  52. package/nitrogen/generated/android/NitroPictureSelectorOnLoad.cpp +41 -0
  53. package/nitrogen/generated/android/NitroPictureSelectorOnLoad.hpp +34 -0
  54. package/nitrogen/generated/android/c++/JCompressOptions.hpp +69 -0
  55. package/nitrogen/generated/android/c++/JCropOptions.hpp +73 -0
  56. package/nitrogen/generated/android/c++/JHybridHybridPictureSelectorSpec.cpp +125 -0
  57. package/nitrogen/generated/android/c++/JHybridHybridPictureSelectorSpec.hpp +64 -0
  58. package/nitrogen/generated/android/c++/JMediaAsset.hpp +98 -0
  59. package/nitrogen/generated/android/c++/JMediaType.hpp +61 -0
  60. package/nitrogen/generated/android/c++/JPickerTheme.hpp +64 -0
  61. package/nitrogen/generated/android/c++/JPictureSelectorOptions.hpp +121 -0
  62. package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/margelo/pictureselector/CompressOptions.kt +47 -0
  63. package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/margelo/pictureselector/CropOptions.kt +50 -0
  64. package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/margelo/pictureselector/HybridHybridPictureSelectorSpec.kt +59 -0
  65. package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/margelo/pictureselector/MediaAsset.kt +68 -0
  66. package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/margelo/pictureselector/MediaType.kt +24 -0
  67. package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/margelo/pictureselector/NitroPictureSelectorOnLoad.kt +35 -0
  68. package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/margelo/pictureselector/PickerTheme.kt +25 -0
  69. package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/margelo/pictureselector/PictureSelectorOptions.kt +65 -0
  70. package/nitrogen/generated/ios/NitroPictureSelector+autolinking.rb +60 -0
  71. package/nitrogen/generated/ios/NitroPictureSelector-Swift-Cxx-Bridge.cpp +49 -0
  72. package/nitrogen/generated/ios/NitroPictureSelector-Swift-Cxx-Bridge.hpp +270 -0
  73. package/nitrogen/generated/ios/NitroPictureSelector-Swift-Cxx-Umbrella.hpp +65 -0
  74. package/nitrogen/generated/ios/c++/HybridHybridPictureSelectorSpecSwift.cpp +11 -0
  75. package/nitrogen/generated/ios/c++/HybridHybridPictureSelectorSpecSwift.hpp +110 -0
  76. package/nitrogen/generated/ios/swift/CompressOptions.swift +83 -0
  77. package/nitrogen/generated/ios/swift/CropOptions.swift +101 -0
  78. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
  79. package/nitrogen/generated/ios/swift/Func_void_std__vector_MediaAsset_.swift +46 -0
  80. package/nitrogen/generated/ios/swift/HybridHybridPictureSelectorSpec.swift +56 -0
  81. package/nitrogen/generated/ios/swift/HybridHybridPictureSelectorSpec_cxx.swift +176 -0
  82. package/nitrogen/generated/ios/swift/MediaAsset.swift +118 -0
  83. package/nitrogen/generated/ios/swift/MediaType.swift +44 -0
  84. package/nitrogen/generated/ios/swift/PickerTheme.swift +48 -0
  85. package/nitrogen/generated/ios/swift/PictureSelectorOptions.swift +182 -0
  86. package/nitrogen/generated/shared/c++/CompressOptions.hpp +95 -0
  87. package/nitrogen/generated/shared/c++/CropOptions.hpp +99 -0
  88. package/nitrogen/generated/shared/c++/HybridHybridPictureSelectorSpec.cpp +22 -0
  89. package/nitrogen/generated/shared/c++/HybridHybridPictureSelectorSpec.hpp +69 -0
  90. package/nitrogen/generated/shared/c++/MediaAsset.hpp +124 -0
  91. package/nitrogen/generated/shared/c++/MediaType.hpp +80 -0
  92. package/nitrogen/generated/shared/c++/PickerTheme.hpp +84 -0
  93. package/nitrogen/generated/shared/c++/PictureSelectorOptions.hpp +132 -0
  94. package/package.json +76 -0
  95. package/src/PictureSelector.ts +72 -0
  96. package/src/index.ts +16 -0
  97. package/src/specs/PictureSelector.nitro.ts +121 -0
  98. package/src/types.ts +38 -0
  99. package/src/usePictureSelector.ts +102 -0
package/README.md ADDED
@@ -0,0 +1,627 @@
1
+ # react-native-picture-selector
2
+
3
+ High-performance photo and video picker for React Native, built on **[Nitro Modules](https://github.com/mrousavy/nitro)** (JSI, zero bridge overhead).
4
+
5
+ | Platform | Native library | Min OS |
6
+ |----------|---------------|--------|
7
+ | Android | [LuckSiege/PictureSelector](https://github.com/LuckSiege/PictureSelector) v3.11.2 | Android 7.0 (API 24) |
8
+ | iOS | [SilenceLove/HXPhotoPicker](https://github.com/SilenceLove/HXPhotoPicker) v5.0.5 | iOS 13.0 |
9
+
10
+ ---
11
+
12
+ ## Features
13
+
14
+ - **Photos and videos** — single or multi-selection, mixed media
15
+ - **Camera capture** — photo and video directly from the picker
16
+ - **Cropping** — fixed ratio, free-style, circular (iOS)
17
+ - **Compression** — JPEG quality + max dimensions
18
+ - **Video duration limits** — min / max in seconds
19
+ - **Pre-selected assets** — restore a previous selection
20
+ - **Themes** — WeChat / White / Dark (Android), hex accent color (iOS)
21
+ - **Strict TypeScript** — fully typed API and result objects
22
+ - **Promise-based** — async/await friendly, cancel → rejection
23
+ - **React hook** — `usePictureSelector` manages loading / error state
24
+
25
+ ---
26
+
27
+ ## Installation
28
+
29
+ ```sh
30
+ npm install react-native-picture-selector react-native-nitro-modules
31
+ # or
32
+ yarn add react-native-picture-selector react-native-nitro-modules
33
+ ```
34
+
35
+ ### iOS — CocoaPods
36
+
37
+ ```sh
38
+ cd ios && pod install
39
+ ```
40
+
41
+ Add required keys to **`ios/YourApp/Info.plist`**:
42
+
43
+ ```xml
44
+ <key>NSPhotoLibraryUsageDescription</key>
45
+ <string>Used to select photos and videos</string>
46
+
47
+ <key>NSPhotoLibraryAddUsageDescription</key>
48
+ <string>Used to save photos to your library</string>
49
+
50
+ <key>NSCameraUsageDescription</key>
51
+ <string>Used to capture photos and videos</string>
52
+
53
+ <key>NSMicrophoneUsageDescription</key>
54
+ <string>Used to record video with audio</string>
55
+ ```
56
+
57
+ ### Android — Gradle
58
+
59
+ Permissions are declared in the library manifest and merged automatically. If you target **Android 13+ (API 33)** and also support older versions you need no extra steps — the manifest already uses `maxSdkVersion` to apply the right permission per API level.
60
+
61
+ Add the FileProvider authority to your app's `AndroidManifest.xml` if you override the default:
62
+
63
+ ```xml
64
+ <!-- Only needed if you customise the FileProvider authority -->
65
+ <provider
66
+ android:name="androidx.core.content.FileProvider"
67
+ android:authorities="${applicationId}.fileprovider"
68
+ android:exported="false"
69
+ android:grantUriPermissions="true">
70
+ <meta-data
71
+ android:name="android.support.FILE_PROVIDER_PATHS"
72
+ android:resource="@xml/file_paths" />
73
+ </provider>
74
+ ```
75
+
76
+ #### Manual package registration (non-autolinking setups)
77
+
78
+ ```kotlin
79
+ // android/app/src/main/java/.../MainApplication.kt
80
+ override fun getPackages() =
81
+ PackageList(this).packages + NitroPictureSelectorPackage()
82
+ ```
83
+
84
+ ---
85
+
86
+ ## Quick start
87
+
88
+ ```tsx
89
+ import { PictureSelector, MediaType } from 'react-native-picture-selector'
90
+
91
+ const assets = await PictureSelector.openPicker({
92
+ mediaType: MediaType.IMAGE,
93
+ maxCount: 1,
94
+ })
95
+
96
+ console.log(assets[0].uri) // file:///...
97
+ console.log(assets[0].width) // 1280
98
+ ```
99
+
100
+ ---
101
+
102
+ ## API Reference
103
+
104
+ ### `PictureSelector.openPicker(options?)`
105
+
106
+ Opens the native gallery picker. Returns a `Promise<MediaAsset[]>`.
107
+
108
+ ```ts
109
+ const assets = await PictureSelector.openPicker({
110
+ mediaType: MediaType.ALL,
111
+ maxCount: 9,
112
+ compress: { enabled: true, quality: 0.7 },
113
+ })
114
+ ```
115
+
116
+ Rejects with `PickerError` when the user dismisses without selecting:
117
+
118
+ ```ts
119
+ try {
120
+ const assets = await PictureSelector.openPicker()
121
+ } catch (e) {
122
+ if ((e as PickerError).code === 'CANCELLED') {
123
+ // user tapped back / cancel — not an error
124
+ }
125
+ }
126
+ ```
127
+
128
+ ---
129
+
130
+ ### `PictureSelector.openCamera(options?)`
131
+
132
+ Opens the device camera for capture. Returns `Promise<MediaAsset[]>` (always one item).
133
+
134
+ ```ts
135
+ const [photo] = await PictureSelector.openCamera({
136
+ mediaType: MediaType.IMAGE,
137
+ crop: { enabled: true, ratioX: 1, ratioY: 1 },
138
+ })
139
+ ```
140
+
141
+ ---
142
+
143
+ ### `usePictureSelector(defaultOptions?)`
144
+
145
+ React hook that manages loading state and the selected asset list.
146
+
147
+ ```tsx
148
+ import { usePictureSelector, MediaType } from 'react-native-picture-selector'
149
+
150
+ function PhotoPicker() {
151
+ const { assets, loading, error, pick, shoot, clear } = usePictureSelector({
152
+ mediaType: MediaType.IMAGE,
153
+ maxCount: 9,
154
+ })
155
+
156
+ return (
157
+ <>
158
+ <Button onPress={() => pick()} title="Gallery" disabled={loading} />
159
+ <Button onPress={() => shoot()} title="Camera" disabled={loading} />
160
+ <Button onPress={clear} title="Clear" />
161
+
162
+ {error && <Text>Error: {error.message}</Text>}
163
+
164
+ {assets.map((a, i) => (
165
+ <Image key={i} source={{ uri: a.uri }} style={{ width: 80, height: 80 }} />
166
+ ))}
167
+ </>
168
+ )
169
+ }
170
+ ```
171
+
172
+ #### Hook state
173
+
174
+ | Property | Type | Description |
175
+ |----------|------|-------------|
176
+ | `assets` | `MediaAsset[]` | Currently selected assets |
177
+ | `loading` | `boolean` | `true` while the picker is open |
178
+ | `error` | `PickerError \| null` | Last non-cancel error |
179
+
180
+ #### Hook actions
181
+
182
+ | Method | Signature | Description |
183
+ |--------|-----------|-------------|
184
+ | `pick` | `(options?) => Promise<MediaAsset[]>` | Open gallery picker |
185
+ | `shoot` | `(options?) => Promise<MediaAsset[]>` | Open camera |
186
+ | `clear` | `() => void` | Reset assets and error |
187
+
188
+ > **Cancellation** — `CANCELLED` errors are swallowed by the hook. `pick()` and `shoot()` return `[]` on cancel instead of throwing.
189
+
190
+ ---
191
+
192
+ ## Configuration (`PictureSelectorOptions`)
193
+
194
+ All fields are optional.
195
+
196
+ ### Media type
197
+
198
+ | Field | Type | Default | Description |
199
+ |-------|------|---------|-------------|
200
+ | `mediaType` | `MediaType` | `IMAGE` | `IMAGE`, `VIDEO`, or `ALL` |
201
+ | `maxCount` | `number` | `1` | Maximum selectable items |
202
+ | `enableCamera` | `boolean` | `true` | Show camera button inside the gallery |
203
+
204
+ ```ts
205
+ import { MediaType } from 'react-native-picture-selector'
206
+
207
+ // images only
208
+ openPicker({ mediaType: MediaType.IMAGE })
209
+
210
+ // videos only
211
+ openPicker({ mediaType: MediaType.VIDEO })
212
+
213
+ // mixed, up to 9
214
+ openPicker({ mediaType: MediaType.ALL, maxCount: 9 })
215
+ ```
216
+
217
+ ---
218
+
219
+ ### Cropping (`crop`)
220
+
221
+ Crop applies only when `maxCount === 1`.
222
+
223
+ | Field | Type | Default | Description |
224
+ |-------|------|---------|-------------|
225
+ | `crop.enabled` | `boolean` | — | Enable crop after selection |
226
+ | `crop.ratioX` | `number` | `1` | Width ratio |
227
+ | `crop.ratioY` | `number` | `1` | Height ratio |
228
+ | `crop.freeStyle` | `boolean` | `false` | Free-form crop (ignores ratio) |
229
+ | `crop.circular` | `boolean` | `false` | Circular crop mask **(iOS only)** |
230
+
231
+ ```ts
232
+ // Square crop
233
+ openPicker({ crop: { enabled: true, ratioX: 1, ratioY: 1 } })
234
+
235
+ // 16:9 crop
236
+ openPicker({ crop: { enabled: true, ratioX: 16, ratioY: 9 } })
237
+
238
+ // Free-style
239
+ openPicker({ crop: { enabled: true, freeStyle: true } })
240
+
241
+ // Circular (iOS)
242
+ openPicker({ crop: { enabled: true, circular: true } })
243
+ ```
244
+
245
+ > Android uses **uCrop**. iOS uses the built-in **HXPhotoPicker editor**.
246
+
247
+ ---
248
+
249
+ ### Compression (`compress`)
250
+
251
+ | Field | Type | Default | Description |
252
+ |-------|------|---------|-------------|
253
+ | `compress.enabled` | `boolean` | — | Enable compression |
254
+ | `compress.quality` | `number` | `0.8` | JPEG quality `0.0`–`1.0` |
255
+ | `compress.maxWidth` | `number` | `1920` | Max output width (px) |
256
+ | `compress.maxHeight` | `number` | `1920` | Max output height (px) |
257
+
258
+ ```ts
259
+ openPicker({
260
+ compress: {
261
+ enabled: true,
262
+ quality: 0.6,
263
+ maxWidth: 1280,
264
+ maxHeight: 1280,
265
+ },
266
+ })
267
+ ```
268
+
269
+ > Android uses **Luban**. iOS uses **HXPhotoPicker** native compression.
270
+
271
+ ---
272
+
273
+ ### Video limits
274
+
275
+ | Field | Type | Description |
276
+ |-------|------|-------------|
277
+ | `maxVideoDuration` | `number` | Max video duration in **seconds** |
278
+ | `minVideoDuration` | `number` | Min video duration in **seconds** |
279
+
280
+ ```ts
281
+ openPicker({
282
+ mediaType: MediaType.VIDEO,
283
+ maxVideoDuration: 60,
284
+ minVideoDuration: 3,
285
+ })
286
+ ```
287
+
288
+ ---
289
+
290
+ ### Themes
291
+
292
+ | Field | Type | Description |
293
+ |-------|------|-------------|
294
+ | `theme` | `PickerTheme` | Preset theme: `DEFAULT`, `WECHAT`, `WHITE`, `DARK` (Android) |
295
+ | `themeColor` | `string` | Hex accent color, e.g. `"#007AFF"` (iOS: `themeColor`; Android: accent) |
296
+
297
+ ```ts
298
+ import { PickerTheme } from 'react-native-picture-selector'
299
+
300
+ // Android WeChat style
301
+ openPicker({ theme: PickerTheme.WECHAT })
302
+
303
+ // Custom accent color (both platforms)
304
+ openPicker({ themeColor: '#1DB954' })
305
+ ```
306
+
307
+ ---
308
+
309
+ ### Pre-selected assets
310
+
311
+ Pass `file://` URIs of previously selected files to restore the selection state:
312
+
313
+ ```ts
314
+ openPicker({
315
+ maxCount: 9,
316
+ selectedAssets: previousAssets.map((a) => a.uri),
317
+ })
318
+ ```
319
+
320
+ ---
321
+
322
+ ## Result (`MediaAsset`)
323
+
324
+ Every item in the returned array has this shape:
325
+
326
+ | Field | Type | Description |
327
+ |-------|------|-------------|
328
+ | `uri` | `string` | `file://` URI of the final file |
329
+ | `type` | `"image" \| "video"` | Media type |
330
+ | `mimeType` | `string` | e.g. `"image/jpeg"`, `"video/mp4"` |
331
+ | `width` | `number` | Width in pixels |
332
+ | `height` | `number` | Height in pixels |
333
+ | `duration` | `number` | Duration in **milliseconds** (0 for images) |
334
+ | `fileName` | `string` | File name with extension |
335
+ | `fileSize` | `number` | Size in bytes |
336
+ | `editedUri?` | `string` | URI after crop/edit (when editing was applied) |
337
+ | `isOriginal?` | `boolean` | iOS: user tapped the Original button |
338
+ | `bucketName?` | `string` | Android: album/folder name |
339
+
340
+ ```ts
341
+ const [asset] = await PictureSelector.openPicker()
342
+
343
+ console.log(asset.uri) // "file:///data/user/0/.../image.jpg"
344
+ console.log(asset.type) // "image"
345
+ console.log(asset.mimeType) // "image/jpeg"
346
+ console.log(asset.width) // 4032
347
+ console.log(asset.height) // 3024
348
+ console.log(asset.fileSize) // 2457600 (bytes)
349
+ console.log(asset.duration) // 0 (image)
350
+ console.log(asset.editedUri) // "file:///..." or undefined
351
+ ```
352
+
353
+ ---
354
+
355
+ ## Error handling
356
+
357
+ ```ts
358
+ import { toPickerError } from 'react-native-picture-selector'
359
+ import type { PickerError } from 'react-native-picture-selector'
360
+
361
+ try {
362
+ const assets = await PictureSelector.openPicker()
363
+ } catch (err) {
364
+ const e = toPickerError(err)
365
+
366
+ switch (e.code) {
367
+ case 'CANCELLED':
368
+ // user dismissed — treat as no-op
369
+ break
370
+ case 'PERMISSION_DENIED':
371
+ // show settings prompt
372
+ break
373
+ case 'UNKNOWN':
374
+ // unexpected error
375
+ console.error(e.message)
376
+ break
377
+ }
378
+ }
379
+ ```
380
+
381
+ | Code | When |
382
+ |------|------|
383
+ | `CANCELLED` | User tapped Back / Cancel |
384
+ | `PERMISSION_DENIED` | Runtime permission not granted |
385
+ | `UNKNOWN` | Any other native error |
386
+
387
+ ---
388
+
389
+ ## TypeScript enums
390
+
391
+ ```ts
392
+ import { MediaType, PickerTheme } from 'react-native-picture-selector'
393
+
394
+ MediaType.IMAGE // 'image'
395
+ MediaType.VIDEO // 'video'
396
+ MediaType.ALL // 'all'
397
+
398
+ PickerTheme.DEFAULT // 'default'
399
+ PickerTheme.WECHAT // 'wechat' (Android)
400
+ PickerTheme.WHITE // 'white' (Android)
401
+ PickerTheme.DARK // 'dark' (Android)
402
+ ```
403
+
404
+ ---
405
+
406
+ ## Common recipes
407
+
408
+ ### Avatar picker (square crop)
409
+
410
+ ```ts
411
+ const [avatar] = await PictureSelector.openPicker({
412
+ mediaType: MediaType.IMAGE,
413
+ maxCount: 1,
414
+ crop: { enabled: true, ratioX: 1, ratioY: 1 },
415
+ compress: { enabled: true, quality: 0.85, maxWidth: 512, maxHeight: 512 },
416
+ })
417
+ ```
418
+
419
+ ### Multi-photo with compression
420
+
421
+ ```ts
422
+ const photos = await PictureSelector.openPicker({
423
+ mediaType: MediaType.IMAGE,
424
+ maxCount: 9,
425
+ compress: { enabled: true, quality: 0.7, maxWidth: 1920, maxHeight: 1920 },
426
+ })
427
+ ```
428
+
429
+ ### Short video clips
430
+
431
+ ```ts
432
+ const [clip] = await PictureSelector.openPicker({
433
+ mediaType: MediaType.VIDEO,
434
+ maxCount: 1,
435
+ minVideoDuration: 1,
436
+ maxVideoDuration: 30,
437
+ })
438
+ ```
439
+
440
+ ### Upload immediately after capture
441
+
442
+ ```ts
443
+ async function captureAndUpload() {
444
+ const [photo] = await PictureSelector.openCamera({
445
+ mediaType: MediaType.IMAGE,
446
+ compress: { enabled: true, quality: 0.8 },
447
+ })
448
+
449
+ const body = new FormData()
450
+ body.append('file', {
451
+ uri: photo.uri,
452
+ type: photo.mimeType,
453
+ name: photo.fileName,
454
+ } as any)
455
+
456
+ await fetch('https://api.example.com/upload', { method: 'POST', body })
457
+ }
458
+ ```
459
+
460
+ ### Chat-style picker with hook
461
+
462
+ ```tsx
463
+ function ChatInput() {
464
+ const { assets, loading, pick, clear } = usePictureSelector({
465
+ mediaType: MediaType.ALL,
466
+ maxCount: 9,
467
+ })
468
+
469
+ const send = async () => {
470
+ if (assets.length === 0) return
471
+ await uploadAll(assets)
472
+ clear()
473
+ }
474
+
475
+ return (
476
+ <View>
477
+ <Pressable onPress={() => pick()} disabled={loading}>
478
+ <Text>📎 Attach</Text>
479
+ </Pressable>
480
+
481
+ {assets.length > 0 && (
482
+ <Pressable onPress={send}>
483
+ <Text>Send ({assets.length})</Text>
484
+ </Pressable>
485
+ )}
486
+ </View>
487
+ )
488
+ }
489
+ ```
490
+
491
+ ---
492
+
493
+ ## Architecture
494
+
495
+ ```
496
+ JavaScript / TypeScript
497
+ PictureSelector.openPicker()
498
+ usePictureSelector()
499
+
500
+ │ JSI (zero serialization overhead)
501
+ │ react-native-nitro-modules
502
+
503
+ HybridPictureSelector
504
+ ┌──────────────────────┬───────────────────────┐
505
+ │ Android (Kotlin) │ iOS (Swift 5.9) │
506
+ │ │ │
507
+ │ PictureSelector v3 │ HXPhotoPicker v5 │
508
+ │ ├─ GlideEngine │ ├─ PickerConfiguration│
509
+ │ ├─ UCropEngine │ ├─ PhotoPickerController│
510
+ │ └─ LubanCompress │ └─ async result map │
511
+ └──────────────────────┴───────────────────────┘
512
+ ```
513
+
514
+ Communication goes through a **statically compiled JSI binding** generated by `nitrogen`. There is no JSON serialization and no async bridge queue — results are returned via native Promises resolved directly on the JS thread.
515
+
516
+ ---
517
+
518
+ ## Development workflow
519
+
520
+ ### Generate native bridge code
521
+
522
+ After modifying `src/specs/PictureSelector.nitro.ts` run:
523
+
524
+ ```sh
525
+ npx nitrogen generate
526
+ # or
527
+ bun run generate
528
+ ```
529
+
530
+ This populates `nitrogen/generated/` with:
531
+ - `android/kotlin/com/nitro/pictureselector/` — Kotlin abstract spec classes
532
+ - `ios/` — Swift protocol files and Objective-C++ bridge
533
+
534
+ **Do not edit files inside `nitrogen/generated/` manually** — they are overwritten on each codegen run.
535
+
536
+ ### Build TypeScript
537
+
538
+ ```sh
539
+ bun run build
540
+ # or
541
+ npx bob build
542
+ ```
543
+
544
+ ### Run example
545
+
546
+ ```sh
547
+ cd example
548
+ npm install
549
+ # iOS
550
+ npx pod-install
551
+ npx react-native run-ios
552
+
553
+ # Android
554
+ npx react-native run-android
555
+ ```
556
+
557
+ ---
558
+
559
+ ## Platform-specific notes
560
+
561
+ ### Android
562
+
563
+ - Minimum SDK: **24** (Android 7.0)
564
+ - PictureSelector v3 handles runtime permissions internally. On Android 13+ it requests `READ_MEDIA_IMAGES` / `READ_MEDIA_VIDEO`. On older versions it requests `READ_EXTERNAL_STORAGE`.
565
+ - The `theme` option uses PictureSelector's built-in style presets. `PickerTheme.WECHAT` is the most polished.
566
+ - Video files captured by camera are placed in the app cache directory.
567
+ - Glide is used for thumbnail rendering inside the gallery grid.
568
+ - uCrop handles all cropping; circular crop uses uCrop's `setCircleDimmedLayer(true)`.
569
+ - Luban handles compression; files under 100 KB are passed through unchanged.
570
+
571
+ ### iOS
572
+
573
+ - Minimum deployment target: **iOS 13.0**
574
+ - Swift 5.9 is required for the C++ interop used by Nitro Modules.
575
+ - HXPhotoPicker requests permissions automatically the first time the picker is opened. The `Info.plist` keys must be present or the app will crash.
576
+ - The `circular` crop option is iOS-only; on Android it falls back to a 1:1 fixed ratio.
577
+ - `isOriginal` in `MediaAsset` is only populated on iOS (when the user taps the "Original" button).
578
+ - `bucketName` is only populated on Android.
579
+ - `themeColor` sets HXPhotoPicker's global `themeColor` property.
580
+ - The `theme` enum values `WECHAT`, `WHITE`, `DARK` have no effect on iOS (only `themeColor` is used).
581
+
582
+ ---
583
+
584
+ ## Permissions summary
585
+
586
+ ### Android
587
+
588
+ | Permission | When requested |
589
+ |-----------|----------------|
590
+ | `CAMERA` | Camera capture |
591
+ | `READ_EXTERNAL_STORAGE` | Android ≤ 12 |
592
+ | `READ_MEDIA_IMAGES` | Android 13+ |
593
+ | `READ_MEDIA_VIDEO` | Android 13+ |
594
+ | `WRITE_EXTERNAL_STORAGE` | Android ≤ 9 only |
595
+
596
+ ### iOS (`Info.plist`)
597
+
598
+ | Key | Purpose |
599
+ |-----|---------|
600
+ | `NSPhotoLibraryUsageDescription` | Read photos/videos |
601
+ | `NSPhotoLibraryAddUsageDescription` | Save to library |
602
+ | `NSCameraUsageDescription` | Camera capture |
603
+ | `NSMicrophoneUsageDescription` | Video with audio |
604
+
605
+ ---
606
+
607
+ ## Known limitations (v1.0)
608
+
609
+ | Feature | Status |
610
+ |---------|--------|
611
+ | Audio file selection | Not supported |
612
+ | iCloud Photos (iOS) | Partial — depends on HXPhotoPicker internals |
613
+ | LivePhoto | Not exposed |
614
+ | Background upload / processing | Out of scope |
615
+ | Save to gallery | Out of scope |
616
+ | Document picker | Out of scope |
617
+
618
+ ---
619
+
620
+ ## License
621
+
622
+ MIT © react-native-picture-selector contributors
623
+
624
+ Native libraries:
625
+ - [LuckSiege/PictureSelector](https://github.com/LuckSiege/PictureSelector) — Apache 2.0
626
+ - [SilenceLove/HXPhotoPicker](https://github.com/SilenceLove/HXPhotoPicker) — MIT
627
+ - [mrousavy/nitro](https://github.com/mrousavy/nitro) — MIT
@@ -0,0 +1,30 @@
1
+ cmake_minimum_required(VERSION 3.22.1)
2
+ project(NitroPictureSelector)
3
+
4
+ # ─── React Native & Nitro Modules ────────────────────────────────────────────
5
+ find_package(ReactAndroid REQUIRED CONFIG)
6
+ find_package(fbjni REQUIRED CONFIG)
7
+ find_package(react-native-nitro-modules REQUIRED CONFIG)
8
+
9
+ # ─── Nitrogen-generated C++ bridge ───────────────────────────────────────────
10
+ # nitrogen writes platform-agnostic JSI glue into nitrogen/generated/android/jni/
11
+ file(GLOB NITRO_GENERATED_SRCS
12
+ "${CMAKE_CURRENT_SOURCE_DIR}/../nitrogen/generated/android/jni/*.cpp"
13
+ )
14
+
15
+ if(NITRO_GENERATED_SRCS)
16
+ add_library(${CMAKE_PROJECT_NAME} SHARED ${NITRO_GENERATED_SRCS})
17
+
18
+ target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE
19
+ "${CMAKE_CURRENT_SOURCE_DIR}/../nitrogen/generated/android/jni"
20
+ )
21
+
22
+ target_link_libraries(${CMAKE_PROJECT_NAME}
23
+ ReactAndroid::jsi
24
+ ReactAndroid::reactnative
25
+ fbjni::fbjni
26
+ react-native-nitro-modules::NitroModules
27
+ android
28
+ log
29
+ )
30
+ endif()