rns-mediapicker 0.0.1
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 +69 -0
- package/android/build.gradle +70 -0
- package/android/src/main/java/com/rnsmediapicker/FastMediaPickerModule.kt +372 -0
- package/android/src/main/java/com/rnsmediapicker/FastMediaPickerPackage.kt +13 -0
- package/android/src/main/res/xml/file_paths.xml +5 -0
- package/app.plugin.js +5 -0
- package/index.d.ts +32 -0
- package/index.js +36 -0
- package/ios/FastMediaPicker.m +11 -0
- package/ios/FastMediaPicker.swift +194 -0
- package/package.json +52 -0
- package/react-native.config.js +11 -0
- package/rns-mediapicker.podspec +18 -0
- package/withMediaPicker.js +116 -0
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
### rns-mediapicker
|
|
2
|
+
A high-performance media picker for React Native and Expo. It handles image/video selection with native efficiency, automatic JPEG compression, and white-background flattening for transparent images.
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
installation
|
|
7
|
+
yarn add rns-mediapicker
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
Add the plugin to your app.json or app.config.js to automate permissions and Android manifest queries.
|
|
12
|
+
|
|
13
|
+
{
|
|
14
|
+
"expo": {
|
|
15
|
+
"plugins": ["rns-mediapicker"]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
import FastMediaPicker from 'rns-mediapicker';
|
|
22
|
+
|
|
23
|
+
const pickMedia = async () => {
|
|
24
|
+
try {
|
|
25
|
+
const result = await FastMediaPicker.pick(
|
|
26
|
+
false, // useCamera: true to open camera, false for gallery
|
|
27
|
+
'both', // mediaType: 'image', 'video', or 'both'
|
|
28
|
+
'back' // env: 'front' or 'back' camera
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
console.log("File URI:", result.uri);
|
|
32
|
+
console.log("Dimensions:", result.width, "x", result.height);
|
|
33
|
+
console.log("Is Video:", result.isVideo);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (error.code === 'E_CANCELLED') {
|
|
36
|
+
console.log('User closed the picker');
|
|
37
|
+
} else {
|
|
38
|
+
console.error('Picker Error:', error.message);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
api-parameters
|
|
46
|
+
pick(useCamera, mediaType, env)
|
|
47
|
+
|
|
48
|
+
1. useCamera (boolean): Default is false.
|
|
49
|
+
2. mediaType (string): 'image', 'video', or 'both'. Default is 'both'.
|
|
50
|
+
3. env (string): 'front' or 'back'. Specifically for camera mode.
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
response-object
|
|
55
|
+
{
|
|
56
|
+
uri: string; // path to the processed file
|
|
57
|
+
width: number; // media width
|
|
58
|
+
height: number; // media height
|
|
59
|
+
type: string; // 'image' or 'video'
|
|
60
|
+
isVideo: boolean; // helper flag
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
-features-included
|
|
64
|
+
1. Android 11+ Intent Queries: Automatically handled via plugin.
|
|
65
|
+
2. FileProvider: Safe URI sharing handled internally.
|
|
66
|
+
3. iOS Permissions: Usage descriptions injected into Info.plist.
|
|
67
|
+
4. Image Flattening: Transparent images are automatically flattened onto white backgrounds.
|
|
68
|
+
5. Swipe-to-Dismiss: iOS swipe-down gestures are correctly caught as cancellations.
|
|
69
|
+
```
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// 1. Buildscript defines WHERE to get the plugins
|
|
2
|
+
buildscript {
|
|
3
|
+
ext.kotlin_version = project.hasProperty('kotlinVersion') ? project.kotlinVersion : '1.8.10'
|
|
4
|
+
repositories {
|
|
5
|
+
google()
|
|
6
|
+
mavenCentral()
|
|
7
|
+
}
|
|
8
|
+
dependencies {
|
|
9
|
+
classpath "com.android.tools.build:gradle:7.4.2"
|
|
10
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// 2. Apply the plugins immediately after buildscript
|
|
15
|
+
apply plugin: 'com.android.library'
|
|
16
|
+
apply plugin: 'kotlin-android'
|
|
17
|
+
|
|
18
|
+
// 3. Define helper functions
|
|
19
|
+
def safeExtGet(prop, fallback) {
|
|
20
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 4. The Android block - should now be recognized
|
|
24
|
+
android {
|
|
25
|
+
// Crucial for AGP 7+
|
|
26
|
+
namespace "com.rnsnativecall"
|
|
27
|
+
|
|
28
|
+
compileSdkVersion safeExtGet('compileSdkVersion', 33)
|
|
29
|
+
|
|
30
|
+
defaultConfig {
|
|
31
|
+
minSdkVersion safeExtGet('minSdkVersion', 21)
|
|
32
|
+
targetSdkVersion safeExtGet('targetSdkVersion', 33)
|
|
33
|
+
|
|
34
|
+
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
compileOptions {
|
|
38
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
39
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
kotlinOptions {
|
|
43
|
+
jvmTarget = '17' // Changed from 1.8
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// lintOptions is deprecated in newer AGP, using lint instead
|
|
47
|
+
lint {
|
|
48
|
+
abortOnError false
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
repositories {
|
|
53
|
+
mavenCentral()
|
|
54
|
+
google()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
dependencies {
|
|
58
|
+
implementation "com.facebook.react:react-native:+"
|
|
59
|
+
|
|
60
|
+
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
61
|
+
implementation "com.google.firebase:firebase-messaging:23.4.0"
|
|
62
|
+
implementation "androidx.appcompat:appcompat:1.6.1"
|
|
63
|
+
|
|
64
|
+
implementation "com.google.android.material:material:1.9.0"
|
|
65
|
+
implementation "androidx.core:core-ktx:1.12.0"
|
|
66
|
+
|
|
67
|
+
// Glide for profile pictures
|
|
68
|
+
implementation "com.github.bumptech.glide:glide:4.15.1"
|
|
69
|
+
annotationProcessor "com.github.bumptech.glide:compiler:4.15.1"
|
|
70
|
+
}
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
package com.rnsmediapicker
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.content.Intent
|
|
5
|
+
import android.graphics.*
|
|
6
|
+
import android.media.MediaMetadataRetriever
|
|
7
|
+
import android.net.Uri
|
|
8
|
+
import android.os.Build
|
|
9
|
+
import android.provider.MediaStore
|
|
10
|
+
import android.webkit.MimeTypeMap
|
|
11
|
+
import androidx.core.content.FileProvider
|
|
12
|
+
import com.facebook.react.bridge.*
|
|
13
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
14
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
15
|
+
import com.facebook.react.modules.core.PermissionAwareActivity
|
|
16
|
+
import com.facebook.react.modules.core.PermissionListener
|
|
17
|
+
import java.io.File
|
|
18
|
+
import java.io.FileOutputStream
|
|
19
|
+
import java.util.*
|
|
20
|
+
|
|
21
|
+
@ReactModule(name = "FastMediaPicker")
|
|
22
|
+
class FastMediaPickerModule(
|
|
23
|
+
private val reactContext: ReactApplicationContext,
|
|
24
|
+
) : ReactContextBaseJavaModule(reactContext),
|
|
25
|
+
ActivityEventListener,
|
|
26
|
+
PermissionListener {
|
|
27
|
+
private var pickerPromise: Promise? = null
|
|
28
|
+
private var cameraCaptureUri: Uri? = null
|
|
29
|
+
private var pendingFrontCamera = false
|
|
30
|
+
|
|
31
|
+
companion object {
|
|
32
|
+
private const val PICKER_REQUEST_CODE = 4123
|
|
33
|
+
private const val CAMERA_PERMISSION_REQUEST = 9123
|
|
34
|
+
private const val MAX_IMAGE_SIZE = 1200
|
|
35
|
+
private const val JPEG_QUALITY = 85
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
init {
|
|
39
|
+
reactContext.addActivityEventListener(this)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
override fun getName(): String = "FastMediaPicker"
|
|
43
|
+
|
|
44
|
+
// -------------------- REQUIRED EVENT CONTRACT --------------------
|
|
45
|
+
|
|
46
|
+
@ReactMethod
|
|
47
|
+
fun addListener(eventName: String) {
|
|
48
|
+
// Required for RN event emitter (even if unused)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@ReactMethod
|
|
52
|
+
fun removeListeners(count: Int) {
|
|
53
|
+
// Required for RN event emitter (even if unused)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private fun sendLog(message: String) {
|
|
57
|
+
android.util.Log.d("FastMediaPicker", message)
|
|
58
|
+
try {
|
|
59
|
+
reactContext
|
|
60
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
61
|
+
.emit("onPickerLog", message)
|
|
62
|
+
} catch (_: Exception) {
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// -------------------- PUBLIC API --------------------
|
|
67
|
+
|
|
68
|
+
@ReactMethod
|
|
69
|
+
fun pick(
|
|
70
|
+
useCamera: Boolean,
|
|
71
|
+
mediaType: String,
|
|
72
|
+
env: String?,
|
|
73
|
+
promise: Promise,
|
|
74
|
+
) {
|
|
75
|
+
val activity =
|
|
76
|
+
currentActivity ?: run {
|
|
77
|
+
promise.reject("E_ACTIVITY_DOES_NOT_EXIST", "Activity doesn't exist")
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
cleanup()
|
|
82
|
+
pickerPromise = promise
|
|
83
|
+
pendingFrontCamera = env?.lowercase() == "front"
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
if (useCamera) {
|
|
87
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
|
|
88
|
+
activity.checkSelfPermission(android.Manifest.permission.CAMERA)
|
|
89
|
+
!= android.content.pm.PackageManager.PERMISSION_GRANTED
|
|
90
|
+
) {
|
|
91
|
+
val permissionAwareActivity =
|
|
92
|
+
activity as? PermissionAwareActivity
|
|
93
|
+
?: run {
|
|
94
|
+
promise.reject("E_ACTIVITY_NOT_PERMISSION_AWARE", "Activity not PermissionAware")
|
|
95
|
+
cleanup()
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
permissionAwareActivity.requestPermissions(
|
|
100
|
+
arrayOf(android.Manifest.permission.CAMERA),
|
|
101
|
+
CAMERA_PERMISSION_REQUEST,
|
|
102
|
+
this,
|
|
103
|
+
)
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
launchCamera(activity, pendingFrontCamera)
|
|
107
|
+
} else {
|
|
108
|
+
launchPicker(activity, mediaType.lowercase())
|
|
109
|
+
}
|
|
110
|
+
} catch (e: Exception) {
|
|
111
|
+
promise.reject("E_LAUNCH_FAILED", e.message)
|
|
112
|
+
cleanup()
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// -------------------- PICKER / CAMERA --------------------
|
|
117
|
+
|
|
118
|
+
private fun launchPicker(
|
|
119
|
+
activity: Activity,
|
|
120
|
+
type: String,
|
|
121
|
+
) {
|
|
122
|
+
val intent =
|
|
123
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
124
|
+
Intent(MediaStore.ACTION_PICK_IMAGES).apply {
|
|
125
|
+
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
126
|
+
this.type =
|
|
127
|
+
when (type) {
|
|
128
|
+
"image" -> "image/*"
|
|
129
|
+
"video" -> "video/*"
|
|
130
|
+
else -> "*/*"
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
|
135
|
+
addCategory(Intent.CATEGORY_OPENABLE)
|
|
136
|
+
addFlags(
|
|
137
|
+
Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
|
138
|
+
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION,
|
|
139
|
+
)
|
|
140
|
+
this.type =
|
|
141
|
+
when (type) {
|
|
142
|
+
"image" -> "image/*"
|
|
143
|
+
"video" -> "video/*"
|
|
144
|
+
else -> "*/*"
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
activity.startActivityForResult(intent, PICKER_REQUEST_CODE)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private fun launchCamera(
|
|
153
|
+
activity: Activity,
|
|
154
|
+
useFrontCamera: Boolean,
|
|
155
|
+
) {
|
|
156
|
+
val photoFile = createTempFile("jpg")
|
|
157
|
+
val authority = "${reactContext.packageName}.provider"
|
|
158
|
+
cameraCaptureUri = FileProvider.getUriForFile(reactContext, authority, photoFile)
|
|
159
|
+
|
|
160
|
+
val intent =
|
|
161
|
+
Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
|
|
162
|
+
putExtra(MediaStore.EXTRA_OUTPUT, cameraCaptureUri)
|
|
163
|
+
addFlags(
|
|
164
|
+
Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
|
165
|
+
Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
|
|
166
|
+
)
|
|
167
|
+
if (useFrontCamera) {
|
|
168
|
+
putExtra("android.intent.extras.CAMERA_FACING", 1)
|
|
169
|
+
putExtra("android.intent.extra.USE_FRONT_CAMERA", true)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
activity.startActivityForResult(intent, PICKER_REQUEST_CODE)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// -------------------- ACTIVITY CALLBACKS --------------------
|
|
177
|
+
|
|
178
|
+
override fun onActivityResult(
|
|
179
|
+
activity: Activity?,
|
|
180
|
+
requestCode: Int,
|
|
181
|
+
resultCode: Int,
|
|
182
|
+
data: Intent?,
|
|
183
|
+
) {
|
|
184
|
+
if (requestCode != PICKER_REQUEST_CODE || pickerPromise == null) return
|
|
185
|
+
|
|
186
|
+
if (resultCode == Activity.RESULT_CANCELED) {
|
|
187
|
+
pickerPromise?.reject("E_CANCELLED", "User cancelled")
|
|
188
|
+
cleanup()
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
val uri = data?.data ?: cameraCaptureUri
|
|
193
|
+
|
|
194
|
+
if (uri != null && Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
|
195
|
+
try {
|
|
196
|
+
reactContext.contentResolver.takePersistableUriPermission(
|
|
197
|
+
uri,
|
|
198
|
+
Intent.FLAG_GRANT_READ_URI_PERMISSION,
|
|
199
|
+
)
|
|
200
|
+
} catch (_: Exception) {
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (uri != null) {
|
|
205
|
+
processMedia(uri)
|
|
206
|
+
} else {
|
|
207
|
+
pickerPromise?.reject("E_NO_URI", "No media selected")
|
|
208
|
+
cleanup()
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
override fun onRequestPermissionsResult(
|
|
213
|
+
requestCode: Int,
|
|
214
|
+
permissions: Array<String>,
|
|
215
|
+
grantResults: IntArray,
|
|
216
|
+
): Boolean {
|
|
217
|
+
if (requestCode == CAMERA_PERMISSION_REQUEST) {
|
|
218
|
+
if (grantResults.isNotEmpty() &&
|
|
219
|
+
grantResults[0] == android.content.pm.PackageManager.PERMISSION_GRANTED
|
|
220
|
+
) {
|
|
221
|
+
currentActivity?.let {
|
|
222
|
+
launchCamera(it, pendingFrontCamera)
|
|
223
|
+
}
|
|
224
|
+
} else {
|
|
225
|
+
pickerPromise?.reject("E_CAMERA_PERMISSION", "Camera permission denied")
|
|
226
|
+
cleanup()
|
|
227
|
+
}
|
|
228
|
+
return true
|
|
229
|
+
}
|
|
230
|
+
return false
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// -------------------- MEDIA PROCESSING --------------------
|
|
234
|
+
|
|
235
|
+
private fun processMedia(uri: Uri) {
|
|
236
|
+
val resolver = reactContext.contentResolver
|
|
237
|
+
val mimeType = resolver.getType(uri) ?: "image/jpeg"
|
|
238
|
+
val isVideo = mimeType.startsWith("video/")
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
if (isVideo) {
|
|
242
|
+
val ext =
|
|
243
|
+
MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) ?: "mp4"
|
|
244
|
+
val tempFile = createTempFile(ext)
|
|
245
|
+
|
|
246
|
+
resolver.openInputStream(uri)?.use { input ->
|
|
247
|
+
FileOutputStream(tempFile).use { output -> input.copyTo(output) }
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
val (w, h) = getVideoDimensions(tempFile)
|
|
251
|
+
resolveResult(Uri.fromFile(tempFile).toString(), true, w, h)
|
|
252
|
+
} else {
|
|
253
|
+
val tempFile = processAndSaveImage(uri)
|
|
254
|
+
val opts = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
|
255
|
+
BitmapFactory.decodeFile(tempFile.absolutePath, opts)
|
|
256
|
+
resolveResult(
|
|
257
|
+
Uri.fromFile(tempFile).toString(),
|
|
258
|
+
false,
|
|
259
|
+
opts.outWidth,
|
|
260
|
+
opts.outHeight,
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
} catch (e: Exception) {
|
|
264
|
+
pickerPromise?.reject("E_PROCESS_ERROR", e.message)
|
|
265
|
+
} finally {
|
|
266
|
+
cleanup()
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private fun getVideoDimensions(file: File): Pair<Int, Int> {
|
|
271
|
+
val retriever = MediaMetadataRetriever()
|
|
272
|
+
return try {
|
|
273
|
+
retriever.setDataSource(file.absolutePath)
|
|
274
|
+
val w =
|
|
275
|
+
retriever
|
|
276
|
+
.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)
|
|
277
|
+
?.toInt() ?: 0
|
|
278
|
+
val h =
|
|
279
|
+
retriever
|
|
280
|
+
.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)
|
|
281
|
+
?.toInt() ?: 0
|
|
282
|
+
val rot =
|
|
283
|
+
retriever
|
|
284
|
+
.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)
|
|
285
|
+
?.toInt() ?: 0
|
|
286
|
+
if (rot == 90 || rot == 270) h to w else w to h
|
|
287
|
+
} finally {
|
|
288
|
+
retriever.release()
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private fun processAndSaveImage(uri: Uri): File {
|
|
293
|
+
val tempFile = createTempFile("jpg")
|
|
294
|
+
val resolver = reactContext.contentResolver
|
|
295
|
+
|
|
296
|
+
val opts =
|
|
297
|
+
BitmapFactory.Options().apply {
|
|
298
|
+
inJustDecodeBounds = true
|
|
299
|
+
resolver.openInputStream(uri)?.use {
|
|
300
|
+
BitmapFactory.decodeStream(it, null, this)
|
|
301
|
+
}
|
|
302
|
+
inSampleSize =
|
|
303
|
+
calculateInSampleSize(outWidth, outHeight, MAX_IMAGE_SIZE, MAX_IMAGE_SIZE)
|
|
304
|
+
inJustDecodeBounds = false
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
val original =
|
|
308
|
+
resolver.openInputStream(uri)?.use {
|
|
309
|
+
BitmapFactory.decodeStream(it, null, opts)
|
|
310
|
+
} ?: throw RuntimeException("Decode failed")
|
|
311
|
+
|
|
312
|
+
val result =
|
|
313
|
+
Bitmap.createBitmap(original.width, original.height, Bitmap.Config.ARGB_8888)
|
|
314
|
+
|
|
315
|
+
Canvas(result).apply {
|
|
316
|
+
drawColor(Color.WHITE)
|
|
317
|
+
drawBitmap(original, 0f, 0f, null)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
FileOutputStream(tempFile).use {
|
|
321
|
+
result.compress(Bitmap.CompressFormat.JPEG, JPEG_QUALITY, it)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
original.recycle()
|
|
325
|
+
result.recycle()
|
|
326
|
+
return tempFile
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private fun calculateInSampleSize(
|
|
330
|
+
w: Int,
|
|
331
|
+
h: Int,
|
|
332
|
+
reqW: Int,
|
|
333
|
+
reqH: Int,
|
|
334
|
+
): Int {
|
|
335
|
+
var size = 1
|
|
336
|
+
if (h > reqH || w > reqW) {
|
|
337
|
+
val halfH = h / 2
|
|
338
|
+
val halfW = w / 2
|
|
339
|
+
while (halfH / size >= reqH && halfW / size >= reqW) {
|
|
340
|
+
size *= 2
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return size
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private fun resolveResult(
|
|
347
|
+
uri: String,
|
|
348
|
+
isVideo: Boolean,
|
|
349
|
+
w: Int,
|
|
350
|
+
h: Int,
|
|
351
|
+
) {
|
|
352
|
+
val map =
|
|
353
|
+
Arguments.createMap().apply {
|
|
354
|
+
putString("uri", uri)
|
|
355
|
+
putBoolean("isVideo", isVideo)
|
|
356
|
+
putString("type", if (isVideo) "video" else "image")
|
|
357
|
+
putDouble("width", w.toDouble())
|
|
358
|
+
putDouble("height", h.toDouble())
|
|
359
|
+
}
|
|
360
|
+
pickerPromise?.resolve(map)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
private fun createTempFile(ext: String): File = File(reactContext.cacheDir, "fast-${UUID.randomUUID()}.$ext")
|
|
364
|
+
|
|
365
|
+
private fun cleanup() {
|
|
366
|
+
pickerPromise = null
|
|
367
|
+
cameraCaptureUri = null
|
|
368
|
+
pendingFrontCamera = false
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
override fun onNewIntent(intent: Intent?) {}
|
|
372
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
package com.rnsmediapicker
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.uimanager.ViewManager
|
|
7
|
+
|
|
8
|
+
class FastMediaPickerPackage : ReactPackage {
|
|
9
|
+
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> =
|
|
10
|
+
listOf(FastMediaPickerModule(reactContext))
|
|
11
|
+
|
|
12
|
+
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> = emptyList()
|
|
13
|
+
}
|
package/app.plugin.js
ADDED
package/index.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
declare module 'rns-mediapicker' {
|
|
2
|
+
export interface PickerResponse {
|
|
3
|
+
/** The local file URI of the processed media */
|
|
4
|
+
uri: string;
|
|
5
|
+
/** Width of the image or video in pixels */
|
|
6
|
+
width: number;
|
|
7
|
+
/** Height of the image or video in pixels */
|
|
8
|
+
height: number;
|
|
9
|
+
/** The type of media: 'image' or 'video' */
|
|
10
|
+
type: 'image' | 'video';
|
|
11
|
+
/** Helper boolean for quick video checking */
|
|
12
|
+
isVideo: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface FastMediaPickerType {
|
|
16
|
+
/**
|
|
17
|
+
* Launch the media picker or camera.
|
|
18
|
+
* * @param useCamera - Set to true to open the camera directly. Defaults to false.
|
|
19
|
+
* @param mediaType - Specify 'image', 'video', or 'both'. Defaults to 'both'.
|
|
20
|
+
* @param env - Use 'front' to specify the front camera on Android/iOS.
|
|
21
|
+
* @returns A promise that resolves with the media metadata.
|
|
22
|
+
*/
|
|
23
|
+
pick(
|
|
24
|
+
useCamera?: boolean,
|
|
25
|
+
mediaType?: 'image' | 'video' | 'both',
|
|
26
|
+
env?: 'front' | 'back' | string
|
|
27
|
+
): Promise<PickerResponse>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const FastMediaPicker: FastMediaPickerType;
|
|
31
|
+
export default FastMediaPicker;
|
|
32
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { NativeModules, TurboModuleRegistry, Platform } from 'react-native';
|
|
2
|
+
|
|
3
|
+
// 1. Try to find the module in the new TurboModuleRegistry or the legacy NativeModules
|
|
4
|
+
const NativeModule = NativeModules.FastMediaPicker
|
|
5
|
+
? NativeModules.FastMediaPicker
|
|
6
|
+
: TurboModuleRegistry.get('FastMediaPicker');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* High-performance media picker for React Native.
|
|
10
|
+
* Supports image and video picking with automatic compression and white-background flattening.
|
|
11
|
+
*/
|
|
12
|
+
const FastMediaPicker = {
|
|
13
|
+
/**
|
|
14
|
+
* Launch the media picker or camera.
|
|
15
|
+
* * @param {boolean} useCamera - Whether to open the camera directly.
|
|
16
|
+
* @param {string} mediaType - 'image', 'video', or 'both'.
|
|
17
|
+
* @param {string} env - Use 'front' for front camera on supported devices.
|
|
18
|
+
* @returns {Promise<{uri: string, width: number, height: number, type: string, isVideo: boolean}>}
|
|
19
|
+
*/
|
|
20
|
+
pick: async (useCamera = false, mediaType = 'both', env = '') => {
|
|
21
|
+
if (!NativeModule) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
"FastMediaPicker: Native module not found. Ensure you have run 'npx expo prebuild' and rebuilt the native app."
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
// In some RN versions, TurboModules require the exact argument count
|
|
29
|
+
return await NativeModule.pick(useCamera, mediaType, env);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default FastMediaPicker;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#import <React/RCTBridgeModule.h>
|
|
2
|
+
|
|
3
|
+
@interface RCT_EXTERN_MODULE(FastMediaPicker, NSObject)
|
|
4
|
+
|
|
5
|
+
RCT_EXTERN_METHOD(pick:(BOOL)useCamera
|
|
6
|
+
mediaType:(NSString *)mediaType
|
|
7
|
+
env:(NSString *)env
|
|
8
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
9
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
10
|
+
|
|
11
|
+
@end
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import AVFoundation
|
|
2
|
+
import Photos
|
|
3
|
+
import UIKit
|
|
4
|
+
import UniformTypeIdentifiers
|
|
5
|
+
import React
|
|
6
|
+
|
|
7
|
+
@objc(FastMediaPicker)
|
|
8
|
+
// 1. Added UIAdaptivePresentationControllerDelegate to the class signature
|
|
9
|
+
class FastMediaPicker: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate, UIAdaptivePresentationControllerDelegate, RCTBridgeModule {
|
|
10
|
+
|
|
11
|
+
static func moduleName() -> String! {
|
|
12
|
+
return "FastMediaPicker"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
static func requiresMainQueueSetup() -> Bool {
|
|
16
|
+
return true
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
private var resolve: RCTPromiseResolveBlock?
|
|
20
|
+
private var reject: RCTPromiseRejectBlock?
|
|
21
|
+
|
|
22
|
+
@objc(pick:mediaType:env:resolver:rejecter:)
|
|
23
|
+
func pick(
|
|
24
|
+
_ useCamera: Bool,
|
|
25
|
+
mediaType: String,
|
|
26
|
+
env: String?,
|
|
27
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
28
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
29
|
+
) {
|
|
30
|
+
self.resolve = resolve
|
|
31
|
+
self.reject = reject
|
|
32
|
+
|
|
33
|
+
DispatchQueue.main.async {
|
|
34
|
+
let picker = UIImagePickerController()
|
|
35
|
+
picker.delegate = self
|
|
36
|
+
// 2. Set the presentation controller delegate to catch swipes
|
|
37
|
+
picker.presentationController?.delegate = self
|
|
38
|
+
picker.allowsEditing = false
|
|
39
|
+
|
|
40
|
+
if useCamera {
|
|
41
|
+
guard UIImagePickerController.isSourceTypeAvailable(.camera) else {
|
|
42
|
+
self.safeReject("E_CAMERA_UNAVAILABLE", "Camera not available")
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
picker.sourceType = .camera
|
|
46
|
+
if let env = env?.lowercased(), env == "front" {
|
|
47
|
+
picker.cameraDevice = .front
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
picker.sourceType = .photoLibrary
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let typeLower = mediaType.lowercased()
|
|
54
|
+
if typeLower == "image" {
|
|
55
|
+
picker.mediaTypes = [UTType.image.identifier]
|
|
56
|
+
} else if typeLower == "video" {
|
|
57
|
+
picker.mediaTypes = [UTType.movie.identifier]
|
|
58
|
+
picker.videoQuality = .typeHigh
|
|
59
|
+
} else {
|
|
60
|
+
picker.mediaTypes = [UTType.image.identifier, UTType.movie.identifier]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if let vc = self.topMostViewController() {
|
|
64
|
+
vc.present(picker, animated: true, completion: nil)
|
|
65
|
+
} else {
|
|
66
|
+
self.safeReject("E_NO_VC", "Could not find view controller")
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// MARK: - Delegate Methods
|
|
72
|
+
|
|
73
|
+
// Handles the "Cancel" button tap
|
|
74
|
+
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
|
|
75
|
+
picker.dismiss(animated: true) {
|
|
76
|
+
self.safeReject("E_CANCELLED", "User cancelled")
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 3. Handles the swipe-down gesture dismissal
|
|
81
|
+
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
|
|
82
|
+
self.safeReject("E_CANCELLED", "User cancelled via swipe")
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
|
|
86
|
+
picker.dismiss(animated: true)
|
|
87
|
+
|
|
88
|
+
if let mediaURL = info[.mediaURL] as? URL {
|
|
89
|
+
handlePickedVideo(url: mediaURL)
|
|
90
|
+
} else if let image = (info[.editedImage] as? UIImage) ?? (info[.originalImage] as? UIImage) {
|
|
91
|
+
processPickedImage(image)
|
|
92
|
+
} else {
|
|
93
|
+
safeReject("E_NO_MEDIA", "Failed to retrieve media")
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// MARK: - Handlers
|
|
98
|
+
private func processPickedImage(_ image: UIImage) {
|
|
99
|
+
let fileURL = makeTempURL(ext: "jpg")
|
|
100
|
+
|
|
101
|
+
// Create a white background to prevent transparent areas from turning black
|
|
102
|
+
let format = UIGraphicsImageRendererFormat()
|
|
103
|
+
format.scale = image.scale
|
|
104
|
+
format.opaque = true // This forces a solid background
|
|
105
|
+
|
|
106
|
+
let renderer = UIGraphicsImageRenderer(size: image.size, format: format)
|
|
107
|
+
let whiteBgImage = renderer.image { context in
|
|
108
|
+
UIColor.white.setFill()
|
|
109
|
+
context.fill(CGRect(origin: .zero, size: image.size))
|
|
110
|
+
image.draw(at: .zero)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Convert the flattened image to JPEG
|
|
114
|
+
guard let data = whiteBgImage.jpegData(compressionQuality: 0.8) else {
|
|
115
|
+
safeReject("E_ENCODE", "JPEG conversion failed")
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
do {
|
|
120
|
+
try data.write(to: fileURL)
|
|
121
|
+
resolve?([
|
|
122
|
+
"uri": fileURL.absoluteString,
|
|
123
|
+
"width": image.size.width,
|
|
124
|
+
"height": image.size.height,
|
|
125
|
+
"type": "image",
|
|
126
|
+
"isVideo": false
|
|
127
|
+
])
|
|
128
|
+
cleanupPromise()
|
|
129
|
+
} catch {
|
|
130
|
+
safeReject("E_WRITE", error.localizedDescription)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private func handlePickedVideo(url: URL) {
|
|
135
|
+
// 4. Force lowercase extension
|
|
136
|
+
let ext = url.pathExtension.isEmpty ? "mp4" : url.pathExtension.lowercased()
|
|
137
|
+
let fileURL = makeTempURL(ext: ext)
|
|
138
|
+
|
|
139
|
+
do {
|
|
140
|
+
if FileManager.default.fileExists(atPath: fileURL.path) {
|
|
141
|
+
try FileManager.default.removeItem(at: fileURL)
|
|
142
|
+
}
|
|
143
|
+
try FileManager.default.copyItem(at: url, to: fileURL)
|
|
144
|
+
let dims = videoDimensions(for: fileURL)
|
|
145
|
+
resolve?([
|
|
146
|
+
"uri": fileURL.absoluteString,
|
|
147
|
+
"width": dims.width,
|
|
148
|
+
"height": dims.height,
|
|
149
|
+
"type": "video",
|
|
150
|
+
"isVideo": true // Added isVideo
|
|
151
|
+
])
|
|
152
|
+
cleanupPromise()
|
|
153
|
+
} catch {
|
|
154
|
+
safeReject("E_VIDEO_COPY", error.localizedDescription)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// MARK: - Helpers
|
|
159
|
+
private func safeReject(_ code: String, _ message: String) {
|
|
160
|
+
self.reject?(code, message, nil)
|
|
161
|
+
cleanupPromise()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private func cleanupPromise() {
|
|
165
|
+
resolve = nil
|
|
166
|
+
reject = nil
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private func topMostViewController() -> UIViewController? {
|
|
170
|
+
let keyWindow = UIApplication.shared.connectedScenes
|
|
171
|
+
.compactMap { $0 as? UIWindowScene }
|
|
172
|
+
.flatMap { $0.windows }
|
|
173
|
+
.first { $0.isKeyWindow }
|
|
174
|
+
|
|
175
|
+
var topController = keyWindow?.rootViewController
|
|
176
|
+
while let presented = topController?.presentedViewController {
|
|
177
|
+
topController = presented
|
|
178
|
+
}
|
|
179
|
+
return topController
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private func makeTempURL(ext: String) -> URL {
|
|
183
|
+
let tempDir = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
|
|
184
|
+
// Ensure ext is lowercase here just in case
|
|
185
|
+
return tempDir.appendingPathComponent("fastpicker-\(UUID().uuidString).\(ext.lowercased())")
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private func videoDimensions(for url: URL) -> (width: CGFloat, height: CGFloat) {
|
|
189
|
+
let asset = AVAsset(url: url)
|
|
190
|
+
guard let track = asset.tracks(withMediaType: .video).first else { return (0, 0) }
|
|
191
|
+
let size = track.naturalSize.applying(track.preferredTransform)
|
|
192
|
+
return (width: abs(size.width), height: abs(size.height))
|
|
193
|
+
}
|
|
194
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rns-mediapicker",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "High-performance React Native module for picking media on Android and iOS.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
7
|
+
"expo": {
|
|
8
|
+
"autolinking": {
|
|
9
|
+
"ios": {
|
|
10
|
+
"podspec": "rns-mediapicker.podspec"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"plugin": "./app.plugin.js"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/raiidr/rns-mediapicker",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": "Raiidr <info@raiidr.com>",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/raiidr/rns-mediapicker.git"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"p": "npm publish --access public"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"react-native",
|
|
27
|
+
"mediapicker",
|
|
28
|
+
"images",
|
|
29
|
+
"videos",
|
|
30
|
+
"android",
|
|
31
|
+
"ios",
|
|
32
|
+
"expo",
|
|
33
|
+
"config-plugin"
|
|
34
|
+
],
|
|
35
|
+
"files": [
|
|
36
|
+
"android",
|
|
37
|
+
"ios",
|
|
38
|
+
"app.plugin.js",
|
|
39
|
+
"index.d.ts",
|
|
40
|
+
"index.js",
|
|
41
|
+
"react-native.config.js",
|
|
42
|
+
"rns-mediapicker.podspec",
|
|
43
|
+
"withMediaPicker.js"
|
|
44
|
+
],
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"expo": ">=45.0.0",
|
|
47
|
+
"react-native": ">=0.60.0"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@expo/config-plugins": "^9.0.0"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = "rns-mediapicker"
|
|
7
|
+
s.version = package["version"]
|
|
8
|
+
s.summary = package["description"]
|
|
9
|
+
s.homepage = package["homepage"]
|
|
10
|
+
s.license = package["license"]
|
|
11
|
+
s.authors = package["author"]
|
|
12
|
+
s.platforms = { :ios => "13.4" }
|
|
13
|
+
s.source = { :git => "https://github.com/raiidr/rns-mediapicker.git", :tag => "#{s.version}" }
|
|
14
|
+
|
|
15
|
+
s.source_files = "ios/**/*.{h,m,mm,swift}"
|
|
16
|
+
|
|
17
|
+
s.dependency "React-Core"
|
|
18
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
const { withAndroidManifest, withInfoPlist, AndroidConfig } = require('@expo/config-plugins');
|
|
2
|
+
|
|
3
|
+
const { getMainApplicationOrThrow } = AndroidConfig.Manifest;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Android Configuration
|
|
7
|
+
*/
|
|
8
|
+
const withAndroidConfig = (config) => {
|
|
9
|
+
return withAndroidManifest(config, (config) => {
|
|
10
|
+
const androidManifest = config.modResults;
|
|
11
|
+
|
|
12
|
+
// 1. Add Permissions
|
|
13
|
+
const permissions = [
|
|
14
|
+
'android.permission.CAMERA',
|
|
15
|
+
'android.permission.READ_EXTERNAL_STORAGE',
|
|
16
|
+
'android.permission.WRITE_EXTERNAL_STORAGE',
|
|
17
|
+
'android.permission.READ_MEDIA_IMAGES',
|
|
18
|
+
'android.permission.READ_MEDIA_VIDEO',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
if (!androidManifest.manifest['uses-permission']) {
|
|
22
|
+
androidManifest.manifest['uses-permission'] = [];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
permissions.forEach((perm) => {
|
|
26
|
+
if (!androidManifest.manifest['uses-permission'].find((p) => p.$['android:name'] === perm)) {
|
|
27
|
+
androidManifest.manifest['uses-permission'].push({ $: { 'android:name': perm } });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// 2. Add Queries (For Android 11+ visibility)
|
|
32
|
+
if (!androidManifest.manifest.queries) {
|
|
33
|
+
androidManifest.manifest.queries = [{}];
|
|
34
|
+
}
|
|
35
|
+
const queries = androidManifest.manifest.queries[0];
|
|
36
|
+
if (!queries.intent) queries.intent = [];
|
|
37
|
+
|
|
38
|
+
const pickerIntents = [
|
|
39
|
+
{ action: [{ $: { 'android:name': 'android.media.action.IMAGE_CAPTURE' } }] },
|
|
40
|
+
{ action: [{ $: { 'android:name': 'android.provider.action.PICK_IMAGES' } }] },
|
|
41
|
+
{
|
|
42
|
+
action: [{ $: { 'android:name': 'android.intent.action.OPEN_DOCUMENT' } }],
|
|
43
|
+
data: [{ $: { 'android:mimeType': 'image/*' } }]
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
action: [{ $: { 'android:name': 'android.intent.action.OPEN_DOCUMENT' } }],
|
|
47
|
+
data: [{ $: { 'android:mimeType': 'video/*' } }]
|
|
48
|
+
}
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
pickerIntents.forEach((intent) => {
|
|
52
|
+
const intentName = intent.action[0].$['android:name'];
|
|
53
|
+
const exists = queries.intent.find(i => i.action[0].$['android:name'] === intentName);
|
|
54
|
+
if (!exists) queries.intent.push(intent);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// 3. Ensure FileProvider exists (The "Null" Fix)
|
|
58
|
+
const mainApplication = getMainApplicationOrThrow(androidManifest);
|
|
59
|
+
if (!mainApplication.provider) {
|
|
60
|
+
mainApplication.provider = [];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const providerClassName = 'androidx.core.content.FileProvider';
|
|
64
|
+
const providerAuthority = '${applicationId}.provider';
|
|
65
|
+
|
|
66
|
+
const hasProvider = mainApplication.provider.find(
|
|
67
|
+
(p) => p.$['android:name'] === providerClassName || p.$['android:authorities'] === providerAuthority
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
if (!hasProvider) {
|
|
71
|
+
mainApplication.provider.push({
|
|
72
|
+
$: {
|
|
73
|
+
'android:name': providerClassName,
|
|
74
|
+
'android:authorities': providerAuthority,
|
|
75
|
+
'android:exported': 'false',
|
|
76
|
+
'android:grantUriPermissions': 'true',
|
|
77
|
+
},
|
|
78
|
+
'meta-data': [
|
|
79
|
+
{
|
|
80
|
+
$: {
|
|
81
|
+
'android:name': 'android.support.FILE_PROVIDER_PATHS',
|
|
82
|
+
'android:resource': '@xml/file_paths',
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return config;
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* iOS Configuration
|
|
95
|
+
*/
|
|
96
|
+
const withIosConfig = (config) => {
|
|
97
|
+
return withInfoPlist(config, (config) => {
|
|
98
|
+
config.modResults.NSCameraUsageDescription =
|
|
99
|
+
config.modResults.NSCameraUsageDescription || "Allow $(PRODUCT_NAME) to access your camera to take photos and videos.";
|
|
100
|
+
config.modResults.NSPhotoLibraryUsageDescription =
|
|
101
|
+
config.modResults.NSPhotoLibraryUsageDescription || "Allow $(PRODUCT_NAME) to access your photo library to select media.";
|
|
102
|
+
config.modResults.NSMicrophoneUsageDescription =
|
|
103
|
+
config.modResults.NSMicrophoneUsageDescription || "Allow $(PRODUCT_NAME) to access your microphone when recording video.";
|
|
104
|
+
|
|
105
|
+
return config;
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Export the Plugin
|
|
111
|
+
*/
|
|
112
|
+
module.exports = function withMediaPicker(config) {
|
|
113
|
+
config = withAndroidConfig(config);
|
|
114
|
+
config = withIosConfig(config);
|
|
115
|
+
return config;
|
|
116
|
+
};
|