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.
- package/README.md +627 -0
- package/android/CMakeLists.txt +30 -0
- package/android/build.gradle +79 -0
- package/android/proguard-rules.pro +21 -0
- package/android/src/main/AndroidManifest.xml +39 -0
- package/android/src/main/kotlin/com/margelo/pictureselector/GlideEngine.kt +80 -0
- package/android/src/main/kotlin/com/margelo/pictureselector/HybridPictureSelector.kt +138 -0
- package/android/src/main/kotlin/com/margelo/pictureselector/LubanCompressEngine.kt +58 -0
- package/android/src/main/kotlin/com/margelo/pictureselector/MediaAssetMapper.kt +69 -0
- package/android/src/main/kotlin/com/margelo/pictureselector/NitroPictureSelectorPackage.kt +52 -0
- package/android/src/main/kotlin/com/margelo/pictureselector/PictureSelectorOptionsMapper.kt +105 -0
- package/android/src/main/kotlin/com/margelo/pictureselector/UCropEngine.kt +57 -0
- package/android/src/main/res/xml/file_paths.xml +8 -0
- package/ios/HybridPictureSelector.swift +386 -0
- package/ios/NitroPictureSelector.podspec +39 -0
- package/lib/commonjs/PictureSelector.js +74 -0
- package/lib/commonjs/PictureSelector.js.map +1 -0
- package/lib/commonjs/index.js +39 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/specs/PictureSelector.nitro.js +34 -0
- package/lib/commonjs/specs/PictureSelector.nitro.js.map +1 -0
- package/lib/commonjs/types.js +44 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/commonjs/usePictureSelector.js +122 -0
- package/lib/commonjs/usePictureSelector.js.map +1 -0
- package/lib/module/PictureSelector.js +71 -0
- package/lib/module/PictureSelector.js.map +1 -0
- package/lib/module/index.js +6 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/specs/PictureSelector.nitro.js +36 -0
- package/lib/module/specs/PictureSelector.nitro.js.map +1 -0
- package/lib/module/types.js +29 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/usePictureSelector.js +119 -0
- package/lib/module/usePictureSelector.js.map +1 -0
- package/lib/typescript/PictureSelector.d.ts +23 -0
- package/lib/typescript/PictureSelector.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +6 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/specs/PictureSelector.nitro.d.ts +96 -0
- package/lib/typescript/specs/PictureSelector.nitro.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +16 -0
- package/lib/typescript/types.d.ts.map +1 -0
- package/lib/typescript/usePictureSelector.d.ts +26 -0
- package/lib/typescript/usePictureSelector.d.ts.map +1 -0
- package/nitro.json +11 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/NitroPictureSelector+autolinking.cmake +81 -0
- package/nitrogen/generated/android/NitroPictureSelector+autolinking.gradle +27 -0
- package/nitrogen/generated/android/NitroPictureSelectorOnLoad.cpp +41 -0
- package/nitrogen/generated/android/NitroPictureSelectorOnLoad.hpp +34 -0
- package/nitrogen/generated/android/c++/JCompressOptions.hpp +69 -0
- package/nitrogen/generated/android/c++/JCropOptions.hpp +73 -0
- package/nitrogen/generated/android/c++/JHybridHybridPictureSelectorSpec.cpp +125 -0
- package/nitrogen/generated/android/c++/JHybridHybridPictureSelectorSpec.hpp +64 -0
- package/nitrogen/generated/android/c++/JMediaAsset.hpp +98 -0
- package/nitrogen/generated/android/c++/JMediaType.hpp +61 -0
- package/nitrogen/generated/android/c++/JPickerTheme.hpp +64 -0
- package/nitrogen/generated/android/c++/JPictureSelectorOptions.hpp +121 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/margelo/pictureselector/CompressOptions.kt +47 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/margelo/pictureselector/CropOptions.kt +50 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/margelo/pictureselector/HybridHybridPictureSelectorSpec.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/margelo/pictureselector/MediaAsset.kt +68 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/margelo/pictureselector/MediaType.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/margelo/pictureselector/NitroPictureSelectorOnLoad.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/margelo/pictureselector/PickerTheme.kt +25 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/margelo/pictureselector/PictureSelectorOptions.kt +65 -0
- package/nitrogen/generated/ios/NitroPictureSelector+autolinking.rb +60 -0
- package/nitrogen/generated/ios/NitroPictureSelector-Swift-Cxx-Bridge.cpp +49 -0
- package/nitrogen/generated/ios/NitroPictureSelector-Swift-Cxx-Bridge.hpp +270 -0
- package/nitrogen/generated/ios/NitroPictureSelector-Swift-Cxx-Umbrella.hpp +65 -0
- package/nitrogen/generated/ios/c++/HybridHybridPictureSelectorSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridHybridPictureSelectorSpecSwift.hpp +110 -0
- package/nitrogen/generated/ios/swift/CompressOptions.swift +83 -0
- package/nitrogen/generated/ios/swift/CropOptions.swift +101 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_MediaAsset_.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridHybridPictureSelectorSpec.swift +56 -0
- package/nitrogen/generated/ios/swift/HybridHybridPictureSelectorSpec_cxx.swift +176 -0
- package/nitrogen/generated/ios/swift/MediaAsset.swift +118 -0
- package/nitrogen/generated/ios/swift/MediaType.swift +44 -0
- package/nitrogen/generated/ios/swift/PickerTheme.swift +48 -0
- package/nitrogen/generated/ios/swift/PictureSelectorOptions.swift +182 -0
- package/nitrogen/generated/shared/c++/CompressOptions.hpp +95 -0
- package/nitrogen/generated/shared/c++/CropOptions.hpp +99 -0
- package/nitrogen/generated/shared/c++/HybridHybridPictureSelectorSpec.cpp +22 -0
- package/nitrogen/generated/shared/c++/HybridHybridPictureSelectorSpec.hpp +69 -0
- package/nitrogen/generated/shared/c++/MediaAsset.hpp +124 -0
- package/nitrogen/generated/shared/c++/MediaType.hpp +80 -0
- package/nitrogen/generated/shared/c++/PickerTheme.hpp +84 -0
- package/nitrogen/generated/shared/c++/PictureSelectorOptions.hpp +132 -0
- package/package.json +76 -0
- package/src/PictureSelector.ts +72 -0
- package/src/index.ts +16 -0
- package/src/specs/PictureSelector.nitro.ts +121 -0
- package/src/types.ts +38 -0
- 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()
|