@onekeyfe/react-native-image-crop-picker 3.0.147
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/LICENSE +21 -0
- package/README.md +83 -0
- package/ReactNativeImageCropPicker.podspec +40 -0
- package/android/CMakeLists.txt +24 -0
- package/android/build.gradle +144 -0
- package/android/gradle.properties +4 -0
- package/android/src/main/AndroidManifest.xml +24 -0
- package/android/src/main/cpp/cpp-adapter.cpp +6 -0
- package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ImageCropPickerException.kt +45 -0
- package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ImageCropPickerImageProcessor.kt +308 -0
- package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ImageCropPickerSession.kt +297 -0
- package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ReactNativeImageCropPicker.kt +69 -0
- package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ReactNativeImageCropPickerPackage.kt +24 -0
- package/ios/ImageCropPickerError.swift +57 -0
- package/ios/ImageCropPickerImageProcessor.swift +343 -0
- package/ios/ImageCropPickerSession.swift +407 -0
- package/ios/ReactNativeImageCropPicker.swift +65 -0
- package/ios/TOCropViewController/Categories/UIImage+CropRotate.h +39 -0
- package/ios/TOCropViewController/Categories/UIImage+CropRotate.m +94 -0
- package/ios/TOCropViewController/Constants/TOCropViewConstants.h +63 -0
- package/ios/TOCropViewController/LICENSE +21 -0
- package/ios/TOCropViewController/Models/TOActivityCroppedImageProvider.h +38 -0
- package/ios/TOCropViewController/Models/TOActivityCroppedImageProvider.m +73 -0
- package/ios/TOCropViewController/Models/TOCropViewControllerAspectRatioPreset.h +64 -0
- package/ios/TOCropViewController/Models/TOCropViewControllerAspectRatioPreset.m +121 -0
- package/ios/TOCropViewController/Models/TOCropViewControllerTransitioning.h +49 -0
- package/ios/TOCropViewController/Models/TOCropViewControllerTransitioning.m +125 -0
- package/ios/TOCropViewController/Models/TOCroppedImageAttributes.h +38 -0
- package/ios/TOCropViewController/Models/TOCroppedImageAttributes.m +45 -0
- package/ios/TOCropViewController/Resources/Base.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/PrivacyInfo.xcprivacy +14 -0
- package/ios/TOCropViewController/Resources/ar.lproj/TOCropViewControllerLocalizable.strings +9 -0
- package/ios/TOCropViewController/Resources/ca.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/cs.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/da-DK.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/de.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/en.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/es.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/fa-IR.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/fa.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/fi.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/fr.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/hu.lproj/TOCropViewControllerLocalizable.strings +9 -0
- package/ios/TOCropViewController/Resources/id.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/it.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/ja.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/ko.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/ms.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/nl.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/pl.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/pt-BR.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/pt.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/ro.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/ru.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/sk.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/tr.lproj/TOCropViewControllerLocalizable.strings +9 -0
- package/ios/TOCropViewController/Resources/uk.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/vi.lproj/TOCropViewControllerLocalizable.strings +9 -0
- package/ios/TOCropViewController/Resources/zh-Hans.lproj/TOCropViewControllerLocalizable.strings +9 -0
- package/ios/TOCropViewController/Resources/zh-Hant.lproj/TOCropViewControllerLocalizable.strings +9 -0
- package/ios/TOCropViewController/TOCropViewController.h +479 -0
- package/ios/TOCropViewController/TOCropViewController.m +1355 -0
- package/ios/TOCropViewController/Views/TOCropOverlayView.h +43 -0
- package/ios/TOCropViewController/Views/TOCropOverlayView.m +240 -0
- package/ios/TOCropViewController/Views/TOCropScrollView.h +39 -0
- package/ios/TOCropViewController/Views/TOCropScrollView.m +48 -0
- package/ios/TOCropViewController/Views/TOCropToolbar.h +104 -0
- package/ios/TOCropViewController/Views/TOCropToolbar.m +783 -0
- package/ios/TOCropViewController/Views/TOCropView.h +311 -0
- package/ios/TOCropViewController/Views/TOCropView.m +1787 -0
- package/lib/module/ReactNativeImageCropPicker.nitro.js +4 -0
- package/lib/module/ReactNativeImageCropPicker.nitro.js.map +1 -0
- package/lib/module/index.js +83 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/ReactNativeImageCropPicker.nitro.d.ts +54 -0
- package/lib/typescript/src/ReactNativeImageCropPicker.nitro.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +31 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/nitro.json +23 -0
- package/nitrogen/generated/android/c++/JCropRect.hpp +69 -0
- package/nitrogen/generated/android/c++/JHybridReactNativeImageCropPickerSpec.cpp +125 -0
- package/nitrogen/generated/android/c++/JHybridReactNativeImageCropPickerSpec.hpp +66 -0
- package/nitrogen/generated/android/c++/JImageCropPickerOptions.hpp +154 -0
- package/nitrogen/generated/android/c++/JPickedImage.hpp +88 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/CropRect.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/HybridReactNativeImageCropPickerSpec.kt +69 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/ImageCropPickerOptions.kt +171 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/PickedImage.kt +86 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/reactnativeimagecroppickerOnLoad.kt +35 -0
- package/nitrogen/generated/android/reactnativeimagecroppicker+autolinking.cmake +81 -0
- package/nitrogen/generated/android/reactnativeimagecroppicker+autolinking.gradle +27 -0
- package/nitrogen/generated/android/reactnativeimagecroppickerOnLoad.cpp +54 -0
- package/nitrogen/generated/android/reactnativeimagecroppickerOnLoad.hpp +34 -0
- package/nitrogen/generated/ios/ReactNativeImageCropPicker+autolinking.rb +62 -0
- package/nitrogen/generated/ios/ReactNativeImageCropPicker-Swift-Cxx-Bridge.cpp +57 -0
- package/nitrogen/generated/ios/ReactNativeImageCropPicker-Swift-Cxx-Bridge.hpp +221 -0
- package/nitrogen/generated/ios/ReactNativeImageCropPicker-Swift-Cxx-Umbrella.hpp +55 -0
- package/nitrogen/generated/ios/ReactNativeImageCropPickerAutolinking.mm +33 -0
- package/nitrogen/generated/ios/ReactNativeImageCropPickerAutolinking.swift +26 -0
- package/nitrogen/generated/ios/c++/HybridReactNativeImageCropPickerSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridReactNativeImageCropPickerSpecSwift.hpp +116 -0
- package/nitrogen/generated/ios/swift/CropRect.swift +44 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_PickedImage.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridReactNativeImageCropPickerSpec.swift +58 -0
- package/nitrogen/generated/ios/swift/HybridReactNativeImageCropPickerSpec_cxx.swift +202 -0
- package/nitrogen/generated/ios/swift/ImageCropPickerOptions.swift +474 -0
- package/nitrogen/generated/ios/swift/PickedImage.swift +96 -0
- package/nitrogen/generated/shared/c++/CropRect.hpp +95 -0
- package/nitrogen/generated/shared/c++/HybridReactNativeImageCropPickerSpec.cpp +24 -0
- package/nitrogen/generated/shared/c++/HybridReactNativeImageCropPickerSpec.hpp +71 -0
- package/nitrogen/generated/shared/c++/ImageCropPickerOptions.hpp +180 -0
- package/nitrogen/generated/shared/c++/PickedImage.hpp +114 -0
- package/package.json +173 -0
- package/src/ReactNativeImageCropPicker.nitro.ts +72 -0
- package/src/index.tsx +134 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
package com.margelo.nitro.reactnativeimagecroppicker
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.Bitmap
|
|
5
|
+
import android.graphics.BitmapFactory
|
|
6
|
+
import android.graphics.Matrix
|
|
7
|
+
import android.net.Uri
|
|
8
|
+
import android.util.Base64
|
|
9
|
+
import androidx.exifinterface.media.ExifInterface
|
|
10
|
+
import java.io.File
|
|
11
|
+
import java.io.FileOutputStream
|
|
12
|
+
import java.io.IOException
|
|
13
|
+
import java.io.InputStream
|
|
14
|
+
import java.util.UUID
|
|
15
|
+
import kotlin.math.floor
|
|
16
|
+
import kotlin.math.max
|
|
17
|
+
import kotlin.math.min
|
|
18
|
+
import kotlin.math.roundToInt
|
|
19
|
+
|
|
20
|
+
internal data class ImageCropPickerConfig(
|
|
21
|
+
val width: Int?,
|
|
22
|
+
val height: Int?,
|
|
23
|
+
val cropping: Boolean,
|
|
24
|
+
val includeBase64: Boolean,
|
|
25
|
+
val compressImageQuality: Double?,
|
|
26
|
+
val compressImageMaxWidth: Double?,
|
|
27
|
+
val compressImageMaxHeight: Double?,
|
|
28
|
+
val freeStyleCropEnabled: Boolean,
|
|
29
|
+
val cropperCircleOverlay: Boolean,
|
|
30
|
+
val cropperToolbarTitle: String?,
|
|
31
|
+
val cropperActiveWidgetColor: String?,
|
|
32
|
+
val cropperToolbarColor: String?,
|
|
33
|
+
val cropperToolbarWidgetColor: String?,
|
|
34
|
+
val cropperStatusBarLight: Boolean,
|
|
35
|
+
val cropperNavigationBarLight: Boolean,
|
|
36
|
+
val showCropGuidelines: Boolean,
|
|
37
|
+
val showCropFrame: Boolean,
|
|
38
|
+
val enableRotationGesture: Boolean,
|
|
39
|
+
val hideBottomControls: Boolean,
|
|
40
|
+
val disableCropperColorSetters: Boolean,
|
|
41
|
+
) {
|
|
42
|
+
companion object {
|
|
43
|
+
fun from(options: ImageCropPickerOptions, forceCropping: Boolean) = ImageCropPickerConfig(
|
|
44
|
+
width = options.width?.takeIf { it.isFinite() }?.roundToInt()?.takeIf { it > 0 },
|
|
45
|
+
height = options.height?.takeIf { it.isFinite() }?.roundToInt()?.takeIf { it > 0 },
|
|
46
|
+
cropping = forceCropping || options.cropping == true,
|
|
47
|
+
includeBase64 = options.includeBase64 == true,
|
|
48
|
+
compressImageQuality = options.compressImageQuality,
|
|
49
|
+
compressImageMaxWidth = options.compressImageMaxWidth,
|
|
50
|
+
compressImageMaxHeight = options.compressImageMaxHeight,
|
|
51
|
+
freeStyleCropEnabled = options.freeStyleCropEnabled == true,
|
|
52
|
+
cropperCircleOverlay = options.cropperCircleOverlay == true,
|
|
53
|
+
cropperToolbarTitle = options.cropperToolbarTitle,
|
|
54
|
+
cropperActiveWidgetColor = options.cropperActiveWidgetColor,
|
|
55
|
+
cropperToolbarColor = options.cropperToolbarColor,
|
|
56
|
+
cropperToolbarWidgetColor = options.cropperToolbarWidgetColor,
|
|
57
|
+
cropperStatusBarLight = options.cropperStatusBarLight ?: true,
|
|
58
|
+
cropperNavigationBarLight = options.cropperNavigationBarLight ?: false,
|
|
59
|
+
showCropGuidelines = options.showCropGuidelines ?: true,
|
|
60
|
+
showCropFrame = options.showCropFrame ?: true,
|
|
61
|
+
enableRotationGesture = options.enableRotationGesture == true,
|
|
62
|
+
hideBottomControls = options.hideBottomControls == true,
|
|
63
|
+
disableCropperColorSetters = options.disableCropperColorSetters == true,
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
internal object ImageCropPickerImageProcessor {
|
|
69
|
+
// Bounds memory for huge photos. Crop targets are far smaller than this.
|
|
70
|
+
private const val MAX_DECODED_PIXEL_SIZE = 4096
|
|
71
|
+
|
|
72
|
+
// Matches react-native-image-crop-picker on Android.
|
|
73
|
+
private const val DEFAULT_COMPRESS_QUALITY = 1.0
|
|
74
|
+
private const val TEMPORARY_DIRECTORY_NAME = "react-native-image-crop-picker"
|
|
75
|
+
|
|
76
|
+
fun temporaryDirectory(context: Context): File {
|
|
77
|
+
val directory = File(context.cacheDir, TEMPORARY_DIRECTORY_NAME)
|
|
78
|
+
if (!directory.isDirectory && !directory.mkdirs()) {
|
|
79
|
+
throw ImageCropPickerException.cannotSaveImage()
|
|
80
|
+
}
|
|
81
|
+
return directory
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
fun createTemporaryFile(context: Context, extension: String): File =
|
|
85
|
+
File(temporaryDirectory(context), "${UUID.randomUUID()}.$extension")
|
|
86
|
+
|
|
87
|
+
fun cleanTemporaryDirectory(context: Context) {
|
|
88
|
+
val directory = File(context.cacheDir, TEMPORARY_DIRECTORY_NAME)
|
|
89
|
+
if (directory.exists() && !directory.deleteRecursively()) {
|
|
90
|
+
throw ImageCropPickerException.cleanupFailed()
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
fun removeFile(path: String) {
|
|
95
|
+
val file = fileFromPath(path) ?: throw ImageCropPickerException.cleanupFailed()
|
|
96
|
+
if (!file.delete()) {
|
|
97
|
+
throw ImageCropPickerException.cleanupFailed()
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private fun fileFromPath(path: String): File? = when {
|
|
102
|
+
path.startsWith("file://") -> Uri.parse(path).path?.let { File(it) }
|
|
103
|
+
path.startsWith("/") -> File(path)
|
|
104
|
+
else -> null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Resolves an openCropper source into a Uri that uCrop can read. Returns the
|
|
108
|
+
// temporary file it had to create, if any, so the caller can delete it.
|
|
109
|
+
fun resolveCropSource(context: Context, path: String): Pair<Uri, File?> {
|
|
110
|
+
when {
|
|
111
|
+
path.startsWith("data:") -> {
|
|
112
|
+
val commaIndex = path.indexOf(',')
|
|
113
|
+
if (commaIndex < 0) {
|
|
114
|
+
throw ImageCropPickerException.cropperImageNotFound()
|
|
115
|
+
}
|
|
116
|
+
val bytes = try {
|
|
117
|
+
Base64.decode(path.substring(commaIndex + 1), Base64.DEFAULT)
|
|
118
|
+
} catch (error: IllegalArgumentException) {
|
|
119
|
+
throw ImageCropPickerException.cropperImageNotFound()
|
|
120
|
+
}
|
|
121
|
+
val file = createTemporaryFile(context, "img")
|
|
122
|
+
try {
|
|
123
|
+
file.writeBytes(bytes)
|
|
124
|
+
} catch (error: IOException) {
|
|
125
|
+
throw ImageCropPickerException.cannotSaveImage(error)
|
|
126
|
+
}
|
|
127
|
+
return Uri.fromFile(file) to file
|
|
128
|
+
}
|
|
129
|
+
// uCrop downloads remote sources and reads content URIs itself.
|
|
130
|
+
path.startsWith("http://") || path.startsWith("https://") || path.startsWith("content://") ->
|
|
131
|
+
return Uri.parse(path) to null
|
|
132
|
+
else -> {
|
|
133
|
+
val file = fileFromPath(path)
|
|
134
|
+
if (file == null || !file.isFile) {
|
|
135
|
+
throw ImageCropPickerException.cropperImageNotFound()
|
|
136
|
+
}
|
|
137
|
+
return Uri.fromFile(file) to null
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Decodes an image with its EXIF orientation applied, downsampled so its
|
|
143
|
+
// longest side is at most MAX_DECODED_PIXEL_SIZE.
|
|
144
|
+
fun decodeBitmap(context: Context, uri: Uri): Bitmap {
|
|
145
|
+
val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
|
146
|
+
openStream(context, uri).use { BitmapFactory.decodeStream(it, null, bounds) }
|
|
147
|
+
if (bounds.outWidth <= 0 || bounds.outHeight <= 0) {
|
|
148
|
+
throw ImageCropPickerException.noImageData("Invalid image selected")
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
var sampleSize = 1
|
|
152
|
+
while (max(bounds.outWidth, bounds.outHeight) / (sampleSize * 2) >= MAX_DECODED_PIXEL_SIZE) {
|
|
153
|
+
sampleSize *= 2
|
|
154
|
+
}
|
|
155
|
+
val options = BitmapFactory.Options().apply { inSampleSize = sampleSize }
|
|
156
|
+
val sampled = try {
|
|
157
|
+
openStream(context, uri).use { BitmapFactory.decodeStream(it, null, options) }
|
|
158
|
+
} catch (error: OutOfMemoryError) {
|
|
159
|
+
throw ImageCropPickerException.lowMemory(error)
|
|
160
|
+
} ?: throw ImageCropPickerException.noImageData("Invalid image selected")
|
|
161
|
+
|
|
162
|
+
val orientation = try {
|
|
163
|
+
openStream(context, uri).use {
|
|
164
|
+
ExifInterface(it).getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
|
|
165
|
+
}
|
|
166
|
+
} catch (error: IOException) {
|
|
167
|
+
ExifInterface.ORIENTATION_NORMAL
|
|
168
|
+
}
|
|
169
|
+
val oriented = applyOrientation(sampled, orientation)
|
|
170
|
+
val scale = min(
|
|
171
|
+
MAX_DECODED_PIXEL_SIZE.toDouble() / oriented.width,
|
|
172
|
+
MAX_DECODED_PIXEL_SIZE.toDouble() / oriented.height,
|
|
173
|
+
)
|
|
174
|
+
return if (scale < 1.0) {
|
|
175
|
+
scaleBitmap(oriented, floorSize(oriented.width * scale), floorSize(oriented.height * scale))
|
|
176
|
+
} else {
|
|
177
|
+
oriented
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Scales a cropped image to the requested size; smaller crops are scaled up.
|
|
182
|
+
// With a locked aspect ratio the crop can still end up a few pixels off that
|
|
183
|
+
// ratio, so the result is scaled to exactly the requested size, as
|
|
184
|
+
// react-native-image-crop-picker did. Free-style crops keep their own aspect
|
|
185
|
+
// ratio and fit inside it.
|
|
186
|
+
fun resizeCroppedBitmap(bitmap: Bitmap, config: ImageCropPickerConfig): Bitmap {
|
|
187
|
+
val width = config.width ?: return bitmap
|
|
188
|
+
val height = config.height ?: return bitmap
|
|
189
|
+
if (!config.freeStyleCropEnabled) {
|
|
190
|
+
return scaleBitmap(bitmap, width, height)
|
|
191
|
+
}
|
|
192
|
+
val widthRatio = width.toDouble() / bitmap.width
|
|
193
|
+
val heightRatio = height.toDouble() / bitmap.height
|
|
194
|
+
return if (widthRatio < heightRatio) {
|
|
195
|
+
scaleBitmap(bitmap, width, roundSize(bitmap.height * widthRatio))
|
|
196
|
+
} else {
|
|
197
|
+
scaleBitmap(bitmap, roundSize(bitmap.width * heightRatio), height)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
fun makeResult(
|
|
202
|
+
context: Context,
|
|
203
|
+
bitmap: Bitmap,
|
|
204
|
+
config: ImageCropPickerConfig,
|
|
205
|
+
cropRect: CropRect?,
|
|
206
|
+
filename: String?,
|
|
207
|
+
): PickedImage {
|
|
208
|
+
var output = bitmap
|
|
209
|
+
val maxWidth = config.compressImageMaxWidth
|
|
210
|
+
val maxHeight = config.compressImageMaxHeight
|
|
211
|
+
val shouldResizeWidth = maxWidth != null && maxWidth < output.width
|
|
212
|
+
val shouldResizeHeight = maxHeight != null && maxHeight < output.height
|
|
213
|
+
if (shouldResizeWidth || shouldResizeHeight) {
|
|
214
|
+
val scale = min(
|
|
215
|
+
(maxWidth ?: output.width.toDouble()) / output.width,
|
|
216
|
+
(maxHeight ?: output.height.toDouble()) / output.height,
|
|
217
|
+
)
|
|
218
|
+
output = scaleBitmap(output, floorSize(output.width * scale), floorSize(output.height * scale))
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
val quality = ((config.compressImageQuality ?: DEFAULT_COMPRESS_QUALITY).coerceIn(0.0, 1.0) * 100).roundToInt()
|
|
222
|
+
val file = createTemporaryFile(context, "jpg")
|
|
223
|
+
try {
|
|
224
|
+
FileOutputStream(file).use { stream ->
|
|
225
|
+
if (!output.compress(Bitmap.CompressFormat.JPEG, quality, stream)) {
|
|
226
|
+
throw ImageCropPickerException.cannotSaveImage()
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
} catch (error: IOException) {
|
|
230
|
+
file.delete()
|
|
231
|
+
throw ImageCropPickerException.cannotSaveImage(error)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
val data = if (config.includeBase64) {
|
|
235
|
+
Base64.encodeToString(file.readBytes(), Base64.NO_WRAP)
|
|
236
|
+
} else {
|
|
237
|
+
null
|
|
238
|
+
}
|
|
239
|
+
return PickedImage(
|
|
240
|
+
path = Uri.fromFile(file).toString(),
|
|
241
|
+
size = file.length().toDouble(),
|
|
242
|
+
width = output.width.toDouble(),
|
|
243
|
+
height = output.height.toDouble(),
|
|
244
|
+
mime = "image/jpeg",
|
|
245
|
+
data = data,
|
|
246
|
+
cropRect = cropRect,
|
|
247
|
+
filename = filename,
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private fun openStream(context: Context, uri: Uri): InputStream =
|
|
252
|
+
try {
|
|
253
|
+
context.contentResolver.openInputStream(uri)
|
|
254
|
+
} catch (error: Exception) {
|
|
255
|
+
null
|
|
256
|
+
} ?: throw ImageCropPickerException.noImageData()
|
|
257
|
+
|
|
258
|
+
private fun floorSize(value: Double): Int = max(1, floor(value).toInt())
|
|
259
|
+
|
|
260
|
+
private fun roundSize(value: Double): Int = max(1, value.roundToInt())
|
|
261
|
+
|
|
262
|
+
private fun scaleBitmap(bitmap: Bitmap, width: Int, height: Int): Bitmap {
|
|
263
|
+
if (width == bitmap.width && height == bitmap.height) {
|
|
264
|
+
return bitmap
|
|
265
|
+
}
|
|
266
|
+
val scaled = try {
|
|
267
|
+
Bitmap.createScaledBitmap(bitmap, width, height, true)
|
|
268
|
+
} catch (error: OutOfMemoryError) {
|
|
269
|
+
throw ImageCropPickerException.lowMemory(error)
|
|
270
|
+
}
|
|
271
|
+
if (scaled !== bitmap) {
|
|
272
|
+
bitmap.recycle()
|
|
273
|
+
}
|
|
274
|
+
return scaled
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private fun applyOrientation(bitmap: Bitmap, orientation: Int): Bitmap {
|
|
278
|
+
val matrix = Matrix()
|
|
279
|
+
when (orientation) {
|
|
280
|
+
ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.setScale(-1f, 1f)
|
|
281
|
+
ExifInterface.ORIENTATION_ROTATE_180 -> matrix.setRotate(180f)
|
|
282
|
+
ExifInterface.ORIENTATION_FLIP_VERTICAL -> {
|
|
283
|
+
matrix.setRotate(180f)
|
|
284
|
+
matrix.postScale(-1f, 1f)
|
|
285
|
+
}
|
|
286
|
+
ExifInterface.ORIENTATION_TRANSPOSE -> {
|
|
287
|
+
matrix.setRotate(90f)
|
|
288
|
+
matrix.postScale(-1f, 1f)
|
|
289
|
+
}
|
|
290
|
+
ExifInterface.ORIENTATION_ROTATE_90 -> matrix.setRotate(90f)
|
|
291
|
+
ExifInterface.ORIENTATION_TRANSVERSE -> {
|
|
292
|
+
matrix.setRotate(-90f)
|
|
293
|
+
matrix.postScale(-1f, 1f)
|
|
294
|
+
}
|
|
295
|
+
ExifInterface.ORIENTATION_ROTATE_270 -> matrix.setRotate(-90f)
|
|
296
|
+
else -> return bitmap
|
|
297
|
+
}
|
|
298
|
+
val oriented = try {
|
|
299
|
+
Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
|
|
300
|
+
} catch (error: OutOfMemoryError) {
|
|
301
|
+
throw ImageCropPickerException.lowMemory(error)
|
|
302
|
+
}
|
|
303
|
+
if (oriented !== bitmap) {
|
|
304
|
+
bitmap.recycle()
|
|
305
|
+
}
|
|
306
|
+
return oriented
|
|
307
|
+
}
|
|
308
|
+
}
|
package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ImageCropPickerSession.kt
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
package com.margelo.nitro.reactnativeimagecroppicker
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.graphics.Bitmap
|
|
5
|
+
import android.graphics.Color
|
|
6
|
+
import android.net.Uri
|
|
7
|
+
import android.os.Handler
|
|
8
|
+
import android.os.Looper
|
|
9
|
+
import android.provider.OpenableColumns
|
|
10
|
+
import androidx.activity.ComponentActivity
|
|
11
|
+
import androidx.activity.result.ActivityResult
|
|
12
|
+
import androidx.activity.result.ActivityResultLauncher
|
|
13
|
+
import androidx.activity.result.PickVisualMediaRequest
|
|
14
|
+
import androidx.activity.result.contract.ActivityResultContract
|
|
15
|
+
import androidx.activity.result.contract.ActivityResultContracts
|
|
16
|
+
import com.margelo.nitro.core.Promise
|
|
17
|
+
import com.margelo.nitro.nativelogger.OneKeyLog
|
|
18
|
+
import com.yalantis.ucrop.UCrop
|
|
19
|
+
import com.yalantis.ucrop.UCropActivity
|
|
20
|
+
import java.io.File
|
|
21
|
+
import java.util.UUID
|
|
22
|
+
import java.util.concurrent.ExecutorService
|
|
23
|
+
import java.util.concurrent.Executors
|
|
24
|
+
|
|
25
|
+
// One picker or cropper flow. It owns the JS promise until the launched
|
|
26
|
+
// activities report back, and settles it exactly once.
|
|
27
|
+
internal class ImageCropPickerSession(
|
|
28
|
+
private val activity: ComponentActivity,
|
|
29
|
+
private val mode: Mode,
|
|
30
|
+
private val config: ImageCropPickerConfig,
|
|
31
|
+
private val promise: Promise<PickedImage>,
|
|
32
|
+
private val onFinish: (ImageCropPickerSession) -> Unit,
|
|
33
|
+
) {
|
|
34
|
+
sealed class Mode {
|
|
35
|
+
object Picker : Mode()
|
|
36
|
+
|
|
37
|
+
class Cropper(val path: String) : Mode()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Main thread state.
|
|
41
|
+
private val keyPrefix = "onekey-image-crop-picker-${UUID.randomUUID()}"
|
|
42
|
+
private val launchers = mutableListOf<ActivityResultLauncher<*>>()
|
|
43
|
+
private val temporaryFiles = mutableListOf<File>()
|
|
44
|
+
private var pickedFilename: String? = null
|
|
45
|
+
private var isAwaitingResult = false
|
|
46
|
+
private var isProcessing = false
|
|
47
|
+
private var isFinished = false
|
|
48
|
+
|
|
49
|
+
// Whether a launched activity or background work is still pending.
|
|
50
|
+
val isActive: Boolean
|
|
51
|
+
get() = !isFinished && (isProcessing || (isAwaitingResult && !activity.isDestroyed))
|
|
52
|
+
|
|
53
|
+
fun start() {
|
|
54
|
+
when (mode) {
|
|
55
|
+
is Mode.Picker -> launchPicker()
|
|
56
|
+
is Mode.Cropper -> resolveCropperSource(mode.path)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Settles a session whose activity went away without calling back.
|
|
61
|
+
fun abandon() {
|
|
62
|
+
finish(Result.failure(ImageCropPickerException.cancelled()))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The system Photo Picker needs no storage or media permission. On devices
|
|
66
|
+
// without it, PickVisualMedia falls back to ACTION_OPEN_DOCUMENT.
|
|
67
|
+
private fun launchPicker() {
|
|
68
|
+
val launcher = register(ActivityResultContracts.PickVisualMedia()) { uri ->
|
|
69
|
+
when {
|
|
70
|
+
uri == null -> finish(Result.failure(ImageCropPickerException.cancelled()))
|
|
71
|
+
config.cropping -> {
|
|
72
|
+
pickedFilename = queryDisplayName(uri)
|
|
73
|
+
launchCropper(uri)
|
|
74
|
+
}
|
|
75
|
+
else -> processPickedImage(uri)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
launch {
|
|
79
|
+
launcher.launch(
|
|
80
|
+
PickVisualMediaRequest.Builder()
|
|
81
|
+
.setMediaType(ActivityResultContracts.PickVisualMedia.ImageOnly)
|
|
82
|
+
.build(),
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private fun processPickedImage(uri: Uri) {
|
|
88
|
+
val filename = queryDisplayName(uri)
|
|
89
|
+
runInBackground(
|
|
90
|
+
work = {
|
|
91
|
+
ImageCropPickerImageProcessor.makeResult(
|
|
92
|
+
activity,
|
|
93
|
+
ImageCropPickerImageProcessor.decodeBitmap(activity, uri),
|
|
94
|
+
config,
|
|
95
|
+
null,
|
|
96
|
+
filename,
|
|
97
|
+
)
|
|
98
|
+
},
|
|
99
|
+
onComplete = ::finish,
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private fun resolveCropperSource(path: String) {
|
|
104
|
+
runInBackground(
|
|
105
|
+
work = { ImageCropPickerImageProcessor.resolveCropSource(activity, path) },
|
|
106
|
+
onComplete = { result ->
|
|
107
|
+
result.fold(
|
|
108
|
+
onSuccess = { (source, temporaryFile) ->
|
|
109
|
+
temporaryFile?.let { temporaryFiles.add(it) }
|
|
110
|
+
launchCropper(source)
|
|
111
|
+
},
|
|
112
|
+
onFailure = { finish(Result.failure(it)) },
|
|
113
|
+
)
|
|
114
|
+
},
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private fun launchCropper(source: Uri) {
|
|
119
|
+
val destination = try {
|
|
120
|
+
ImageCropPickerImageProcessor.createTemporaryFile(activity, "jpg")
|
|
121
|
+
} catch (error: ImageCropPickerException) {
|
|
122
|
+
finish(Result.failure(error))
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
temporaryFiles.add(destination)
|
|
126
|
+
|
|
127
|
+
val cropper = UCrop.of(source, Uri.fromFile(destination)).withOptions(buildCropOptions())
|
|
128
|
+
if (config.width != null && config.height != null) {
|
|
129
|
+
cropper.withAspectRatio(config.width.toFloat(), config.height.toFloat())
|
|
130
|
+
}
|
|
131
|
+
val launcher = register(ActivityResultContracts.StartActivityForResult()) { result ->
|
|
132
|
+
handleCropResult(result)
|
|
133
|
+
}
|
|
134
|
+
launch { launcher.launch(cropper.getIntent(activity)) }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private fun handleCropResult(result: ActivityResult) {
|
|
138
|
+
val data = result.data
|
|
139
|
+
when (result.resultCode) {
|
|
140
|
+
Activity.RESULT_OK -> {
|
|
141
|
+
val output = data?.let { UCrop.getOutput(it) }
|
|
142
|
+
if (data == null || output == null) {
|
|
143
|
+
finish(Result.failure(ImageCropPickerException.noImageData()))
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
val cropRect = CropRect(
|
|
147
|
+
x = data.getIntExtra(UCrop.EXTRA_OUTPUT_OFFSET_X, -1).toDouble(),
|
|
148
|
+
y = data.getIntExtra(UCrop.EXTRA_OUTPUT_OFFSET_Y, -1).toDouble(),
|
|
149
|
+
width = data.getIntExtra(UCrop.EXTRA_OUTPUT_IMAGE_WIDTH, -1).toDouble(),
|
|
150
|
+
height = data.getIntExtra(UCrop.EXTRA_OUTPUT_IMAGE_HEIGHT, -1).toDouble(),
|
|
151
|
+
)
|
|
152
|
+
val filename = pickedFilename
|
|
153
|
+
runInBackground(
|
|
154
|
+
work = {
|
|
155
|
+
val cropped = ImageCropPickerImageProcessor.decodeBitmap(activity, output)
|
|
156
|
+
ImageCropPickerImageProcessor.makeResult(
|
|
157
|
+
activity,
|
|
158
|
+
ImageCropPickerImageProcessor.resizeCroppedBitmap(cropped, config),
|
|
159
|
+
config,
|
|
160
|
+
cropRect,
|
|
161
|
+
filename,
|
|
162
|
+
)
|
|
163
|
+
},
|
|
164
|
+
onComplete = ::finish,
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
UCrop.RESULT_ERROR -> {
|
|
168
|
+
val message = data?.let { UCrop.getError(it)?.message } ?: "Cannot crop image"
|
|
169
|
+
finish(Result.failure(ImageCropPickerException.noImageData(message)))
|
|
170
|
+
}
|
|
171
|
+
else -> finish(Result.failure(ImageCropPickerException.cancelled()))
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private fun buildCropOptions(): UCrop.Options = UCrop.Options().apply {
|
|
176
|
+
setCompressionFormat(Bitmap.CompressFormat.JPEG)
|
|
177
|
+
setCompressionQuality(100)
|
|
178
|
+
setCircleDimmedLayer(config.cropperCircleOverlay)
|
|
179
|
+
setFreeStyleCropEnabled(config.freeStyleCropEnabled)
|
|
180
|
+
setShowCropGrid(config.showCropGuidelines)
|
|
181
|
+
setShowCropFrame(config.showCropFrame)
|
|
182
|
+
setHideBottomControls(config.hideBottomControls)
|
|
183
|
+
config.cropperToolbarTitle?.let { setToolbarTitle(it) }
|
|
184
|
+
if (config.enableRotationGesture) {
|
|
185
|
+
setAllowedGestures(UCropActivity.ALL, UCropActivity.ALL, UCropActivity.ALL)
|
|
186
|
+
}
|
|
187
|
+
if (!config.disableCropperColorSetters) {
|
|
188
|
+
parseColor(config.cropperActiveWidgetColor)?.let { setActiveControlsWidgetColor(it) }
|
|
189
|
+
parseColor(config.cropperToolbarColor)?.let { setToolbarColor(it) }
|
|
190
|
+
parseColor(config.cropperToolbarWidgetColor)?.let { setToolbarWidgetColor(it) }
|
|
191
|
+
setStatusBarLight(config.cropperStatusBarLight)
|
|
192
|
+
setNavigationBarLight(config.cropperNavigationBarLight)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private fun <I, O> register(
|
|
197
|
+
contract: ActivityResultContract<I, O>,
|
|
198
|
+
callback: (O) -> Unit,
|
|
199
|
+
): ActivityResultLauncher<I> {
|
|
200
|
+
val key = "$keyPrefix-${launchers.size}"
|
|
201
|
+
val launcher = activity.activityResultRegistry.register(key, contract) { output ->
|
|
202
|
+
isAwaitingResult = false
|
|
203
|
+
if (!isFinished) {
|
|
204
|
+
callback(output)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
launchers.add(launcher)
|
|
208
|
+
return launcher
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private fun launch(block: () -> Unit) {
|
|
212
|
+
isAwaitingResult = true
|
|
213
|
+
try {
|
|
214
|
+
block()
|
|
215
|
+
} catch (error: Exception) {
|
|
216
|
+
isAwaitingResult = false
|
|
217
|
+
finish(Result.failure(ImageCropPickerException.failedToShowPicker(error)))
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private fun <T> runInBackground(work: () -> T, onComplete: (Result<T>) -> Unit) {
|
|
222
|
+
isProcessing = true
|
|
223
|
+
executor.execute {
|
|
224
|
+
val result = try {
|
|
225
|
+
Result.success(work())
|
|
226
|
+
} catch (error: ImageCropPickerException) {
|
|
227
|
+
Result.failure(error)
|
|
228
|
+
} catch (error: OutOfMemoryError) {
|
|
229
|
+
Result.failure(ImageCropPickerException.lowMemory(error))
|
|
230
|
+
} catch (error: Exception) {
|
|
231
|
+
Result.failure(ImageCropPickerException.noImageData(error.message ?: "Cannot process image"))
|
|
232
|
+
}
|
|
233
|
+
mainHandler.post {
|
|
234
|
+
isProcessing = false
|
|
235
|
+
if (!isFinished) {
|
|
236
|
+
onComplete(result)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private fun finish(result: Result<PickedImage>) {
|
|
243
|
+
if (isFinished) {
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
isFinished = true
|
|
247
|
+
isAwaitingResult = false
|
|
248
|
+
|
|
249
|
+
// Unregister outside of the registry's dispatch callback.
|
|
250
|
+
val finishedLaunchers = launchers.toList()
|
|
251
|
+
launchers.clear()
|
|
252
|
+
mainHandler.post { finishedLaunchers.forEach { it.unregister() } }
|
|
253
|
+
|
|
254
|
+
val filesToDelete = temporaryFiles.toList()
|
|
255
|
+
temporaryFiles.clear()
|
|
256
|
+
if (filesToDelete.isNotEmpty()) {
|
|
257
|
+
executor.execute { filesToDelete.forEach { it.delete() } }
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
result.fold(
|
|
261
|
+
onSuccess = { promise.resolve(it) },
|
|
262
|
+
onFailure = { error ->
|
|
263
|
+
if ((error as? ImageCropPickerException)?.code != ImageCropPickerException.E_PICKER_CANCELLED) {
|
|
264
|
+
OneKeyLog.warn(TAG, error.message ?: error.toString())
|
|
265
|
+
}
|
|
266
|
+
promise.reject(error)
|
|
267
|
+
},
|
|
268
|
+
)
|
|
269
|
+
onFinish(this)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private fun queryDisplayName(uri: Uri): String? {
|
|
273
|
+
val displayName = try {
|
|
274
|
+
activity.contentResolver
|
|
275
|
+
.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)
|
|
276
|
+
?.use { cursor -> if (cursor.moveToFirst()) cursor.getString(0) else null }
|
|
277
|
+
} catch (error: Exception) {
|
|
278
|
+
null
|
|
279
|
+
}
|
|
280
|
+
return displayName ?: uri.lastPathSegment
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private fun parseColor(value: String?): Int? =
|
|
284
|
+
value?.let {
|
|
285
|
+
try {
|
|
286
|
+
Color.parseColor(it)
|
|
287
|
+
} catch (error: IllegalArgumentException) {
|
|
288
|
+
null
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
companion object {
|
|
293
|
+
private const val TAG = "ImageCropPicker"
|
|
294
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
295
|
+
private val executor: ExecutorService = Executors.newSingleThreadExecutor()
|
|
296
|
+
}
|
|
297
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
package com.margelo.nitro.reactnativeimagecroppicker
|
|
2
|
+
|
|
3
|
+
import android.os.Handler
|
|
4
|
+
import android.os.Looper
|
|
5
|
+
import androidx.activity.ComponentActivity
|
|
6
|
+
import com.facebook.proguard.annotations.DoNotStrip
|
|
7
|
+
import com.margelo.nitro.NitroModules
|
|
8
|
+
import com.margelo.nitro.core.Promise
|
|
9
|
+
|
|
10
|
+
@DoNotStrip
|
|
11
|
+
class ReactNativeImageCropPicker : HybridReactNativeImageCropPickerSpec() {
|
|
12
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
13
|
+
|
|
14
|
+
// Main thread only.
|
|
15
|
+
private var activeSession: ImageCropPickerSession? = null
|
|
16
|
+
|
|
17
|
+
override fun openPicker(options: ImageCropPickerOptions): Promise<PickedImage> =
|
|
18
|
+
startSession(
|
|
19
|
+
ImageCropPickerSession.Mode.Picker,
|
|
20
|
+
ImageCropPickerConfig.from(options, forceCropping = false),
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
override fun openCropper(path: String, options: ImageCropPickerOptions): Promise<PickedImage> =
|
|
24
|
+
startSession(
|
|
25
|
+
ImageCropPickerSession.Mode.Cropper(path),
|
|
26
|
+
ImageCropPickerConfig.from(options, forceCropping = true),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
override fun clean(): Promise<Unit> =
|
|
30
|
+
Promise.parallel {
|
|
31
|
+
val context = NitroModules.applicationContext ?: throw ImageCropPickerException.cleanupFailed()
|
|
32
|
+
ImageCropPickerImageProcessor.cleanTemporaryDirectory(context)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
override fun cleanSingle(path: String): Promise<Unit> =
|
|
36
|
+
Promise.parallel { ImageCropPickerImageProcessor.removeFile(path) }
|
|
37
|
+
|
|
38
|
+
private fun startSession(
|
|
39
|
+
mode: ImageCropPickerSession.Mode,
|
|
40
|
+
config: ImageCropPickerConfig,
|
|
41
|
+
): Promise<PickedImage> {
|
|
42
|
+
val promise = Promise<PickedImage>()
|
|
43
|
+
mainHandler.post {
|
|
44
|
+
activeSession?.let { session ->
|
|
45
|
+
if (session.isActive) {
|
|
46
|
+
promise.reject(ImageCropPickerException.inProgress())
|
|
47
|
+
return@post
|
|
48
|
+
}
|
|
49
|
+
// Its activity went away without calling back; don't block new requests on it.
|
|
50
|
+
session.abandon()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
val activity = NitroModules.applicationContext?.currentActivity as? ComponentActivity
|
|
54
|
+
if (activity == null || activity.isFinishing || activity.isDestroyed) {
|
|
55
|
+
promise.reject(ImageCropPickerException.noActivity())
|
|
56
|
+
return@post
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
val session = ImageCropPickerSession(activity, mode, config, promise) { finishedSession ->
|
|
60
|
+
if (activeSession === finishedSession) {
|
|
61
|
+
activeSession = null
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
activeSession = session
|
|
65
|
+
session.start()
|
|
66
|
+
}
|
|
67
|
+
return promise
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
package com.margelo.nitro.reactnativeimagecroppicker
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.BaseReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
7
|
+
|
|
8
|
+
class ReactNativeImageCropPickerPackage : BaseReactPackage() {
|
|
9
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
|
|
10
|
+
return null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
|
|
14
|
+
return ReactModuleInfoProvider { HashMap() }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
companion object {
|
|
18
|
+
init {
|
|
19
|
+
System.loadLibrary("reactnativeimagecroppicker")
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|