react-native-customizable-image-crop-picker 1.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.
Files changed (176) hide show
  1. package/LICENSE +44 -0
  2. package/README.md +479 -0
  3. package/RNCustomizableImageCropPicker.podspec +26 -0
  4. package/android/.gradle/8.9/checksums/checksums.lock +0 -0
  5. package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
  6. package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
  7. package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
  8. package/android/.gradle/8.9/gc.properties +0 -0
  9. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  10. package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
  11. package/android/.gradle/vcs-1/gc.properties +0 -0
  12. package/android/build/.transforms/a34a6297c7e9b65c987429ecd018e9a9/results.bin +1 -0
  13. package/android/build/.transforms/a34a6297c7e9b65c987429ecd018e9a9/transformed/classes/classes_dex/classes.dex +0 -0
  14. package/android/build/.transforms/d07257a7807cb64bf14f5adb0f98ee25/results.bin +1 -0
  15. package/android/build/.transforms/d07257a7807cb64bf14f5adb0f98ee25/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/rncustomizableimagecroppicker/BuildConfig.dex +0 -0
  16. package/android/build/.transforms/d07257a7807cb64bf14f5adb0f98ee25/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/rncustomizableimagecroppicker/NativeImageCropperActivity$Companion.dex +0 -0
  17. package/android/build/.transforms/d07257a7807cb64bf14f5adb0f98ee25/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/rncustomizableimagecroppicker/NativeImageCropperActivity.dex +0 -0
  18. package/android/build/.transforms/d07257a7807cb64bf14f5adb0f98ee25/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/rncustomizableimagecroppicker/NativeImageCropperModule$Companion.dex +0 -0
  19. package/android/build/.transforms/d07257a7807cb64bf14f5adb0f98ee25/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/rncustomizableimagecroppicker/NativeImageCropperModule.dex +0 -0
  20. package/android/build/.transforms/d07257a7807cb64bf14f5adb0f98ee25/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/rncustomizableimagecroppicker/NativeImageCropperPackage.dex +0 -0
  21. package/android/build/.transforms/d07257a7807cb64bf14f5adb0f98ee25/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/rncustomizableimagecroppicker/NativeImagePickerFileProvider.dex +0 -0
  22. package/android/build/.transforms/d07257a7807cb64bf14f5adb0f98ee25/transformed/bundleLibRuntimeToDirDebug/desugar_graph.bin +0 -0
  23. package/android/build/generated/source/buildConfig/debug/com/rncustomizableimagecroppicker/BuildConfig.java +10 -0
  24. package/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/AndroidManifest.xml +26 -0
  25. package/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/output-metadata.json +18 -0
  26. package/android/build/intermediates/aar_metadata/debug/writeDebugAarMetadata/aar-metadata.properties +6 -0
  27. package/android/build/intermediates/annotation_processor_list/debug/javaPreCompileDebug/annotationProcessors.json +1 -0
  28. package/android/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar +0 -0
  29. package/android/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar +0 -0
  30. package/android/build/intermediates/compile_symbol_list/debug/generateDebugRFile/R.txt +1 -0
  31. package/android/build/intermediates/compiled_local_resources/debug/compileDebugLibraryResources/out/xml_nativeimagepicker_file_paths.xml.flat +0 -0
  32. package/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties +2 -0
  33. package/android/build/intermediates/incremental/debug/packageDebugResources/merger.xml +2 -0
  34. package/android/build/intermediates/incremental/mergeDebugAssets/merger.xml +2 -0
  35. package/android/build/intermediates/incremental/mergeDebugJniLibFolders/merger.xml +2 -0
  36. package/android/build/intermediates/incremental/mergeDebugShaders/merger.xml +2 -0
  37. package/android/build/intermediates/java_res/debug/processDebugJavaRes/out/META-INF/react-native-customizable-image-crop-picker_debug.kotlin_module +0 -0
  38. package/android/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/rncustomizableimagecroppicker/BuildConfig.class +0 -0
  39. package/android/build/intermediates/local_only_symbol_list/debug/parseDebugLocalResources/R-def.txt +3 -0
  40. package/android/build/intermediates/manifest_merge_blame_file/debug/processDebugManifest/manifest-merger-blame-debug-report.txt +41 -0
  41. package/android/build/intermediates/merged_manifest/debug/processDebugManifest/AndroidManifest.xml +26 -0
  42. package/android/build/intermediates/navigation_json/debug/extractDeepLinksDebug/navigation.json +1 -0
  43. package/android/build/intermediates/nested_resources_validation_report/debug/generateDebugResources/nestedResourcesValidationReport.txt +1 -0
  44. package/android/build/intermediates/packaged_res/debug/packageDebugResources/xml/nativeimagepicker_file_paths.xml +6 -0
  45. package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/META-INF/react-native-customizable-image-crop-picker_debug.kotlin_module +0 -0
  46. package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/rncustomizableimagecroppicker/BuildConfig.class +0 -0
  47. package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/rncustomizableimagecroppicker/NativeImageCropperActivity$Companion.class +0 -0
  48. package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/rncustomizableimagecroppicker/NativeImageCropperActivity.class +0 -0
  49. package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/rncustomizableimagecroppicker/NativeImageCropperModule$Companion.class +0 -0
  50. package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/rncustomizableimagecroppicker/NativeImageCropperModule.class +0 -0
  51. package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/rncustomizableimagecroppicker/NativeImageCropperPackage.class +0 -0
  52. package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/rncustomizableimagecroppicker/NativeImagePickerFileProvider.class +0 -0
  53. package/android/build/intermediates/runtime_library_classes_jar/debug/bundleLibRuntimeToJarDebug/classes.jar +0 -0
  54. package/android/build/intermediates/symbol_list_with_package_name/debug/generateDebugRFile/package-aware-r.txt +2 -0
  55. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab +0 -0
  56. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream +0 -0
  57. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream.len +0 -0
  58. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.len +0 -0
  59. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.values.at +0 -0
  60. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i +0 -0
  61. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i.len +0 -0
  62. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab +0 -0
  63. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream +0 -0
  64. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream.len +0 -0
  65. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.len +0 -0
  66. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.values.at +0 -0
  67. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i +0 -0
  68. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i.len +0 -0
  69. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab +0 -0
  70. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream +0 -0
  71. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream.len +0 -0
  72. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.len +0 -0
  73. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.values.at +0 -0
  74. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i +0 -0
  75. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i.len +0 -0
  76. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab +0 -0
  77. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.keystream +0 -0
  78. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.keystream.len +0 -0
  79. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.len +0 -0
  80. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.values +0 -0
  81. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.values.at +0 -0
  82. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.values.s +1 -0
  83. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab_i +0 -0
  84. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab_i.len +0 -0
  85. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab +0 -0
  86. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream +0 -0
  87. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream.len +0 -0
  88. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.len +0 -0
  89. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.values.at +0 -0
  90. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i +0 -0
  91. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i.len +0 -0
  92. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab +0 -0
  93. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream +0 -0
  94. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream.len +0 -0
  95. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.len +0 -0
  96. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values +0 -0
  97. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values.at +0 -0
  98. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values.s +1 -0
  99. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i +0 -0
  100. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i.len +0 -0
  101. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab +0 -0
  102. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream +0 -0
  103. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream.len +0 -0
  104. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.len +0 -0
  105. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.values.at +0 -0
  106. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab_i +0 -0
  107. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab_i.len +0 -0
  108. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab +0 -0
  109. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.keystream +0 -0
  110. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.keystream.len +0 -0
  111. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.len +0 -0
  112. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.values.at +0 -0
  113. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab_i +0 -0
  114. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab_i.len +0 -0
  115. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab +0 -0
  116. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.keystream +0 -0
  117. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.keystream.len +0 -0
  118. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.len +0 -0
  119. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.values.at +0 -0
  120. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab_i +0 -0
  121. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab_i.len +0 -0
  122. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/counters.tab +2 -0
  123. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab +0 -0
  124. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream +0 -0
  125. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream.len +0 -0
  126. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.len +0 -0
  127. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.values.at +0 -0
  128. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab_i +0 -0
  129. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab_i.len +0 -0
  130. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab +0 -0
  131. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream +0 -0
  132. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream.len +0 -0
  133. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.len +0 -0
  134. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.values.at +0 -0
  135. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_i +0 -0
  136. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_i.len +0 -0
  137. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab +0 -0
  138. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream +0 -0
  139. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream.len +0 -0
  140. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.len +0 -0
  141. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.values +0 -0
  142. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.values.at +0 -0
  143. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.values.s +1 -0
  144. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab_i +0 -0
  145. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab_i.len +0 -0
  146. package/android/build/kotlin/compileDebugKotlin/cacheable/last-build.bin +0 -0
  147. package/android/build/kotlin/compileDebugKotlin/classpath-snapshot/shrunk-classpath-snapshot.bin +0 -0
  148. package/android/build/kotlin/compileDebugKotlin/local-state/build-history.bin +0 -0
  149. package/android/build/outputs/logs/manifest-merger-debug-report.txt +46 -0
  150. package/android/build/tmp/compileDebugJavaWithJavac/previous-compilation-data.bin +0 -0
  151. package/android/build/tmp/kotlin-classes/debug/META-INF/react-native-customizable-image-crop-picker_debug.kotlin_module +0 -0
  152. package/android/build/tmp/kotlin-classes/debug/com/rncustomizableimagecroppicker/NativeImageCropperActivity$Companion.class +0 -0
  153. package/android/build/tmp/kotlin-classes/debug/com/rncustomizableimagecroppicker/NativeImageCropperActivity.class +0 -0
  154. package/android/build/tmp/kotlin-classes/debug/com/rncustomizableimagecroppicker/NativeImageCropperModule$Companion.class +0 -0
  155. package/android/build/tmp/kotlin-classes/debug/com/rncustomizableimagecroppicker/NativeImageCropperModule.class +0 -0
  156. package/android/build/tmp/kotlin-classes/debug/com/rncustomizableimagecroppicker/NativeImageCropperPackage.class +0 -0
  157. package/android/build/tmp/kotlin-classes/debug/com/rncustomizableimagecroppicker/NativeImagePickerFileProvider.class +0 -0
  158. package/android/build.gradle +49 -0
  159. package/android/consumer-rules.pro +3 -0
  160. package/android/src/main/AndroidManifest.xml +23 -0
  161. package/android/src/main/java/com/rncustomizableimagecroppicker/NativeImageCropperActivity.kt +1009 -0
  162. package/android/src/main/java/com/rncustomizableimagecroppicker/NativeImageCropperModule.kt +684 -0
  163. package/android/src/main/java/com/rncustomizableimagecroppicker/NativeImageCropperPackage.kt +17 -0
  164. package/android/src/main/java/com/rncustomizableimagecroppicker/NativeImagePickerFileProvider.kt +6 -0
  165. package/android/src/main/res/xml/nativeimagepicker_file_paths.xml +6 -0
  166. package/ios/NativeImageCropperModule.m +13 -0
  167. package/ios/NativeImageCropperModule.swift +1400 -0
  168. package/package.json +116 -0
  169. package/react-native.config.js +13 -0
  170. package/src/api.ts +41 -0
  171. package/src/errors.ts +39 -0
  172. package/src/index.ts +4 -0
  173. package/src/native/NativeImageCropperModule.ts +34 -0
  174. package/src/native/mapOptions.ts +164 -0
  175. package/src/types.ts +258 -0
  176. package/src/ui/ImageCropPickerModal.tsx +322 -0
@@ -0,0 +1,684 @@
1
+ package com.rncustomizableimagecroppicker
2
+
3
+ import android.app.Activity
4
+ import android.Manifest
5
+ import android.content.Intent
6
+ import android.graphics.Bitmap
7
+ import android.graphics.Color
8
+ import android.net.Uri
9
+ import android.provider.MediaStore
10
+ import android.util.Base64
11
+ import android.util.Base64OutputStream
12
+ import androidx.activity.result.PickVisualMediaRequest
13
+ import androidx.activity.result.contract.ActivityResultContracts
14
+ import androidx.core.content.ContextCompat
15
+ import androidx.core.content.FileProvider
16
+ import com.facebook.react.bridge.Promise
17
+ import com.facebook.react.bridge.ReactApplicationContext
18
+ import com.facebook.react.bridge.ActivityEventListener
19
+ import com.facebook.react.bridge.ReactContextBaseJavaModule
20
+ import com.facebook.react.bridge.ReactMethod
21
+ import com.facebook.react.bridge.ReadableMap
22
+ import com.facebook.react.bridge.WritableNativeMap
23
+ import com.facebook.react.modules.core.DeviceEventManagerModule
24
+ import com.facebook.react.module.annotations.ReactModule
25
+ import com.yalantis.ucrop.UCrop
26
+ import java.io.ByteArrayOutputStream
27
+ import java.io.File
28
+ import java.util.UUID
29
+ import java.util.concurrent.ExecutorService
30
+ import java.util.concurrent.Executors
31
+
32
+ @ReactModule(name = NativeImageCropperModule.NAME)
33
+ class NativeImageCropperModule(private val reactContext: ReactApplicationContext) :
34
+ ReactContextBaseJavaModule(reactContext), ActivityEventListener {
35
+
36
+ companion object {
37
+ const val NAME = "NativeImageCropperModule"
38
+ private const val REQUEST_PICK_IMAGE = 63201
39
+ private const val REQUEST_CAPTURE_IMAGE = 63202
40
+
41
+ private const val E_ACTIVITY_DOES_NOT_EXIST = "E_ACTIVITY_DOES_NOT_EXIST"
42
+ private const val E_PICKER_CANCELLED = "E_PICKER_CANCELLED"
43
+ private const val E_PICKER_CANCELLED_MSG = "User cancelled image selection"
44
+ private const val E_NO_IMAGE_DATA_FOUND = "E_NO_IMAGE_DATA_FOUND"
45
+ private const val E_MODULE_DESTROYED = "E_MODULE_DESTROYED"
46
+ private const val E_PICKER_ERROR = "E_PICKER_ERROR"
47
+ private const val E_PERMISSION_MISSING = "E_PERMISSION_MISSING"
48
+ private const val E_NO_APP_AVAILABLE = "E_NO_APP_AVAILABLE"
49
+
50
+ private const val DEFAULT_CROP_SIZE = 100
51
+ private const val SOURCE_CAMERA = "camera"
52
+ private const val SOURCE_GALLERY = "gallery"
53
+ }
54
+
55
+ override fun getName(): String = NAME
56
+
57
+ // Required by React Native when using NativeEventEmitter on Android.
58
+ // (No-op; we emit events opportunistically during base64 encoding.)
59
+ @ReactMethod
60
+ fun addListener(eventName: String) = Unit
61
+
62
+ @ReactMethod
63
+ fun removeListeners(count: Int) = Unit
64
+
65
+ private var pendingPromise: Promise? = null
66
+ private val encodeExecutor: ExecutorService = Executors.newSingleThreadExecutor()
67
+ @Volatile private var isInvalidated = false
68
+
69
+ private var cropWidth = DEFAULT_CROP_SIZE
70
+ private var cropHeight = DEFAULT_CROP_SIZE
71
+ private var headerTitle = "Preview Image"
72
+ private var cancelText = "Cancel"
73
+ private var uploadText = "Upload"
74
+ // Defaults aligned with the "demo button layout":
75
+ // - Cancel is a light button with dark text
76
+ // - Upload is a black button with white text
77
+ private var cancelColor = "#111111" // text color
78
+ private var uploadColor = "#111111" // background color
79
+ private var dimmedLayerColor = "#B3000000"
80
+ private var statusBarColor = "#FFFFFF"
81
+ private var statusBarStyle = "auto" // "dark" | "light" | "auto"
82
+ private var isDarkTheme = false
83
+ private var drawUnderStatusBar = false
84
+ private var pickerSource = SOURCE_GALLERY
85
+ private var pendingCameraUri: Uri? = null
86
+ private var includeBase64 = false
87
+ private var freeStyleCropEnabled = false
88
+ private var compressQuality = 100 // 0..100
89
+ private var compressFormat = "jpeg" // jpeg | png | webp
90
+ private var circularCrop = false
91
+ private var rotationEnabled = false
92
+ private var cropGridEnabled = false
93
+ private var cropFrameColor = ""
94
+ private var cropGridColor = ""
95
+ private var showNativeCropControls = false
96
+
97
+ private var headerBackgroundColor = "#FFFFFF"
98
+ private var headerTitleColor = "#111111"
99
+ private var headerTitleFontSize = 22
100
+ private var headerTitleFontFamily = ""
101
+ private var headerHeight = 84
102
+ private var headerPaddingHorizontal = 20
103
+ private var headerPaddingTop = 20
104
+ private var headerPaddingBottom = 20
105
+
106
+ private var buttonContainerBackgroundColor = "#FFFFFF"
107
+ private var buttonContainerPaddingHorizontal = 20
108
+ private var buttonContainerPaddingTop = 16
109
+ private var buttonContainerPaddingBottom = 24
110
+ private var buttonGap = 12
111
+ private var buttonHeight = 54
112
+ private var buttonLayout = "vertical"
113
+
114
+ private var cancelButtonBackgroundColor = "#EFEFF4"
115
+ private var cancelButtonBorderColor = "#EFEFF4"
116
+ private var cancelButtonBorderWidth = 0
117
+ private var cancelButtonFontSize = 18
118
+ private var cancelButtonFontFamily = ""
119
+ private var cancelButtonRadius = 28
120
+
121
+ private var uploadButtonTextColor = "#FFFFFF"
122
+ private var uploadButtonBackgroundColor = "#111111"
123
+ private var uploadButtonBorderColor = "#111111"
124
+ private var uploadButtonBorderWidth = 0
125
+ private var uploadButtonFontSize = 18
126
+ private var uploadButtonFontFamily = ""
127
+ private var uploadButtonRadius = 28
128
+
129
+ private var controlsPlacement = "bottom"
130
+ private var topLeftControl = "cancel"
131
+ private var topRightControl = "upload"
132
+ private var footerButtonOrder = "uploadFirst"
133
+
134
+ private var cancelButtonContent = "text"
135
+ private var cancelButtonIconUri = ""
136
+ private var cancelButtonIconBase64 = ""
137
+ private var cancelButtonIconTintColor = ""
138
+ private var cancelButtonIconSize = 18
139
+ private var cancelButtonIconGap = 8
140
+ private var cancelButtonPaddingHorizontal = 12
141
+ private var cancelButtonPaddingVertical = 0
142
+
143
+ private var uploadButtonContent = "text"
144
+ private var uploadButtonIconUri = ""
145
+ private var uploadButtonIconBase64 = ""
146
+ private var uploadButtonIconTintColor = ""
147
+ private var uploadButtonIconSize = 18
148
+ private var uploadButtonIconGap = 8
149
+ private var uploadButtonPaddingHorizontal = 12
150
+ private var uploadButtonPaddingVertical = 0
151
+
152
+ private var headerAlignment = "left" // "left" | "center" | "right"
153
+
154
+ init {
155
+ reactContext.addActivityEventListener(this)
156
+ }
157
+
158
+ @ReactMethod
159
+ fun openImagePreview(options: ReadableMap, promise: Promise) {
160
+ if (isInvalidated) {
161
+ promise.reject(E_MODULE_DESTROYED, "Native image cropper module is destroyed")
162
+ return
163
+ }
164
+ val activity = reactApplicationContext.currentActivity
165
+ if (activity == null) {
166
+ promise.reject(E_ACTIVITY_DOES_NOT_EXIST, "Activity doesn't exist")
167
+ return
168
+ }
169
+ if (pendingPromise != null) {
170
+ promise.reject(E_PICKER_ERROR, "Image picker is already in progress")
171
+ return
172
+ }
173
+
174
+ applyConfig(options)
175
+ pendingPromise = promise
176
+
177
+ try {
178
+ if (pickerSource == SOURCE_CAMERA) {
179
+ launchCamera(activity)
180
+ } else {
181
+ launchSystemPhotoPicker(activity)
182
+ }
183
+ } catch (error: Exception) {
184
+ rejectPending(E_PICKER_ERROR, error.message ?: "Unable to open image picker")
185
+ }
186
+ }
187
+
188
+ override fun onActivityResult(activity: Activity, requestCode: Int, resultCode: Int, data: Intent?) {
189
+ when (requestCode) {
190
+ REQUEST_PICK_IMAGE -> handleImagePickResult(activity, resultCode, data)
191
+ REQUEST_CAPTURE_IMAGE -> handleCameraResult(activity, resultCode, data)
192
+ UCrop.REQUEST_CROP -> handleCropResult(resultCode, data)
193
+ }
194
+ }
195
+
196
+ override fun onNewIntent(intent: Intent) = Unit
197
+
198
+ override fun invalidate() {
199
+ isInvalidated = true
200
+ reactContext.removeActivityEventListener(this)
201
+ pendingCameraUri = null
202
+ rejectPending(E_MODULE_DESTROYED, "Native image cropper module is destroyed")
203
+ encodeExecutor.shutdownNow()
204
+ super.invalidate()
205
+ }
206
+
207
+ private fun applyConfig(options: ReadableMap) {
208
+ isDarkTheme = options.hasKey("isDarkTheme") && options.getBoolean("isDarkTheme")
209
+ includeBase64 = options.hasKey("includeBase64") && options.getBoolean("includeBase64")
210
+ compressQuality = runCatching {
211
+ if (options.hasKey("compressQuality") && !options.isNull("compressQuality")) {
212
+ val q = options.getDouble("compressQuality").coerceIn(0.0, 1.0)
213
+ kotlin.math.round(q * 100.0).toInt().coerceIn(0, 100)
214
+ } else {
215
+ 100
216
+ }
217
+ }.getOrDefault(100)
218
+ compressFormat = options.getStringOrDefault("compressFormat", "jpeg").let { raw ->
219
+ when (raw.lowercase()) {
220
+ "png" -> "png"
221
+ "webp" -> "webp"
222
+ else -> "jpeg"
223
+ }
224
+ }
225
+ circularCrop = options.hasKey("circularCrop") && options.getBoolean("circularCrop")
226
+ rotationEnabled = options.hasKey("rotationEnabled") && options.getBoolean("rotationEnabled")
227
+ cropGridEnabled = options.hasKey("cropGridEnabled") && options.getBoolean("cropGridEnabled")
228
+ cropFrameColor = options.getStringOrDefault("cropFrameColor", "")
229
+ cropGridColor = options.getStringOrDefault("cropGridColor", "")
230
+ showNativeCropControls = options.hasKey("showNativeCropControls") && options.getBoolean("showNativeCropControls")
231
+ if (rotationEnabled) showNativeCropControls = true
232
+ drawUnderStatusBar = options.hasKey("drawUnderStatusBar") && options.getBoolean("drawUnderStatusBar")
233
+ freeStyleCropEnabled = options.hasKey("freeStyleCropEnabled") && options.getBoolean("freeStyleCropEnabled")
234
+ cropWidth = sanitizePositive(options.getIntOrDefault("width", DEFAULT_CROP_SIZE), DEFAULT_CROP_SIZE)
235
+ cropHeight = sanitizePositive(options.getIntOrDefault("height", DEFAULT_CROP_SIZE), DEFAULT_CROP_SIZE)
236
+ headerTitle = options.getStringOrDefault("cropperToolbarTitle", "Preview Image")
237
+ cancelText = options.getStringOrDefault("cropperCancelText", "Cancel")
238
+ uploadText = options.getStringOrDefault("cropperChooseText", "Upload")
239
+ cancelColor = options.getStringOrDefault("cropperCancelColor", "#111111")
240
+ uploadColor = options.getStringOrDefault("cropperChooseColor", "#111111")
241
+ dimmedLayerColor = options.getStringOrDefault(
242
+ "cropperDimmedLayerColor",
243
+ if (isDarkTheme) "#E0000000" else "#B3000000",
244
+ )
245
+ pickerSource = options.getStringOrDefault("pickerSource", SOURCE_GALLERY)
246
+
247
+ controlsPlacement = options.getStringOrDefault("controlsPlacement", "bottom").let { raw ->
248
+ if (raw == "top") "top" else "bottom"
249
+ }
250
+ if (showNativeCropControls) controlsPlacement = "top"
251
+ topLeftControl = options.getStringOrDefault("topLeftControl", "cancel").let { raw ->
252
+ if (raw == "upload" || raw == "cancel" || raw == "none") raw else "cancel"
253
+ }
254
+ topRightControl = options.getStringOrDefault("topRightControl", "upload").let { raw ->
255
+ if (raw == "upload" || raw == "cancel" || raw == "none") raw else "upload"
256
+ }
257
+ footerButtonOrder = options.getStringOrDefault("footerButtonOrder", "uploadFirst").let { raw ->
258
+ if (raw == "cancelFirst") "cancelFirst" else "uploadFirst"
259
+ }
260
+
261
+ headerAlignment = options.getStringOrDefault("headerAlignment", "left").let { raw ->
262
+ if (raw == "center" || raw == "right" || raw == "left") raw else "left"
263
+ }
264
+
265
+ val headerStyle = getOptionalMap(options, "headerStyle")
266
+ headerBackgroundColor = getMapString(headerStyle, "backgroundColor", if (isDarkTheme) "#0B0B0F" else "#FFFFFF")
267
+ headerTitleColor = getMapString(
268
+ headerStyle,
269
+ "color",
270
+ getMapString(headerStyle, "titleColor", if (isDarkTheme) "#F5F5F7" else "#111111"),
271
+ )
272
+ headerTitleFontSize = sanitizePositive(getMapInt(headerStyle, "fontSize", 22), 22)
273
+ headerTitleFontFamily = getMapString(headerStyle, "fontFamily", "")
274
+ val defaultHeaderHeight = if (controlsPlacement == "top") 84 else 56
275
+ headerHeight = sanitizePositive(getMapInt(headerStyle, "height", defaultHeaderHeight), defaultHeaderHeight)
276
+ headerPaddingHorizontal = sanitizeNonNegative(getMapInt(headerStyle, "paddingHorizontal", 20), 20)
277
+ val defaultHeaderPadTop = if (controlsPlacement == "top") 20 else 12
278
+ val defaultHeaderPadBottom = if (controlsPlacement == "top") 20 else 12
279
+ headerPaddingTop = sanitizeNonNegative(getMapInt(headerStyle, "paddingTop", defaultHeaderPadTop), defaultHeaderPadTop)
280
+ headerPaddingBottom = sanitizeNonNegative(getMapInt(headerStyle, "paddingBottom", defaultHeaderPadBottom), defaultHeaderPadBottom)
281
+
282
+ // Default status bar color to header background to avoid a visible seam.
283
+ // Explicit `statusBarColor` (cropperStatusBarColor) still overrides this.
284
+ statusBarColor = options.getStringOrDefault("cropperStatusBarColor", headerBackgroundColor)
285
+ statusBarStyle = options.getStringOrDefault("cropperStatusBarStyle", "auto").let { raw ->
286
+ if (raw == "dark" || raw == "light") raw else "auto"
287
+ }
288
+
289
+ val containerStyle = getOptionalMap(options, "buttonContainerStyle")
290
+ buttonContainerBackgroundColor = getMapString(containerStyle, "backgroundColor", "#FFFFFF")
291
+ buttonContainerPaddingHorizontal = sanitizeNonNegative(getMapInt(containerStyle, "paddingHorizontal", 20), 20)
292
+ buttonContainerPaddingTop = sanitizeNonNegative(getMapInt(containerStyle, "paddingTop", 16), 16)
293
+ buttonContainerPaddingBottom = sanitizeNonNegative(getMapInt(containerStyle, "paddingBottom", 24), 24)
294
+ buttonGap = sanitizeNonNegative(getMapInt(containerStyle, "gap", 12), 12)
295
+ buttonHeight = sanitizePositive(getMapInt(containerStyle, "buttonHeight", 54), 54)
296
+ buttonLayout = getMapString(containerStyle, "layout", "vertical").let { raw ->
297
+ if (raw == "horizontal") "horizontal" else "vertical"
298
+ }
299
+
300
+ val cancelStyle = getOptionalMap(options, "cancelButtonStyle")
301
+ cancelButtonBackgroundColor = getMapString(cancelStyle, "backgroundColor", cancelButtonBackgroundColor)
302
+ cancelButtonBorderColor = getMapString(cancelStyle, "borderColor", cancelButtonBorderColor)
303
+ cancelButtonBorderWidth = sanitizeNonNegative(getMapInt(cancelStyle, "borderWidth", cancelButtonBorderWidth), cancelButtonBorderWidth)
304
+ cancelButtonFontSize = sanitizePositive(getMapInt(cancelStyle, "fontSize", cancelButtonFontSize), cancelButtonFontSize)
305
+ cancelButtonFontFamily = getMapString(cancelStyle, "fontFamily", cancelButtonFontFamily)
306
+ cancelButtonRadius = sanitizeNonNegative(getMapInt(cancelStyle, "borderRadius", cancelButtonRadius), cancelButtonRadius)
307
+ cancelColor = getMapString(cancelStyle, "textColor", cancelColor)
308
+ cancelButtonContent = getMapString(cancelStyle, "content", cancelButtonContent)
309
+ cancelButtonIconUri = getMapString(cancelStyle, "iconUri", cancelButtonIconUri)
310
+ cancelButtonIconBase64 = getMapString(cancelStyle, "iconBase64", cancelButtonIconBase64)
311
+ cancelButtonIconTintColor = getMapString(cancelStyle, "iconTintColor", cancelButtonIconTintColor)
312
+ cancelButtonIconSize = sanitizePositive(getMapInt(cancelStyle, "iconSize", cancelButtonIconSize), cancelButtonIconSize)
313
+ cancelButtonIconGap = sanitizeNonNegative(getMapInt(cancelStyle, "iconGap", cancelButtonIconGap), cancelButtonIconGap)
314
+ cancelButtonPaddingHorizontal = sanitizeNonNegative(getMapInt(cancelStyle, "paddingHorizontal", cancelButtonPaddingHorizontal), cancelButtonPaddingHorizontal)
315
+ cancelButtonPaddingVertical = sanitizeNonNegative(getMapInt(cancelStyle, "paddingVertical", cancelButtonPaddingVertical), cancelButtonPaddingVertical)
316
+
317
+ val uploadStyle = getOptionalMap(options, "uploadButtonStyle")
318
+ uploadButtonBackgroundColor = getMapString(uploadStyle, "backgroundColor", uploadButtonBackgroundColor)
319
+ uploadButtonBorderColor = getMapString(uploadStyle, "borderColor", uploadButtonBorderColor)
320
+ uploadButtonBorderWidth = sanitizeNonNegative(getMapInt(uploadStyle, "borderWidth", uploadButtonBorderWidth), uploadButtonBorderWidth)
321
+ uploadButtonFontSize = sanitizePositive(getMapInt(uploadStyle, "fontSize", uploadButtonFontSize), uploadButtonFontSize)
322
+ uploadButtonFontFamily = getMapString(uploadStyle, "fontFamily", uploadButtonFontFamily)
323
+ uploadButtonRadius = sanitizeNonNegative(getMapInt(uploadStyle, "borderRadius", uploadButtonRadius), uploadButtonRadius)
324
+ uploadButtonTextColor = getMapString(uploadStyle, "textColor", uploadButtonTextColor)
325
+ uploadColor = uploadButtonBackgroundColor
326
+ uploadButtonContent = getMapString(uploadStyle, "content", uploadButtonContent)
327
+ uploadButtonIconUri = getMapString(uploadStyle, "iconUri", uploadButtonIconUri)
328
+ uploadButtonIconBase64 = getMapString(uploadStyle, "iconBase64", uploadButtonIconBase64)
329
+ uploadButtonIconTintColor = getMapString(uploadStyle, "iconTintColor", uploadButtonIconTintColor)
330
+ uploadButtonIconSize = sanitizePositive(getMapInt(uploadStyle, "iconSize", uploadButtonIconSize), uploadButtonIconSize)
331
+ uploadButtonIconGap = sanitizeNonNegative(getMapInt(uploadStyle, "iconGap", uploadButtonIconGap), uploadButtonIconGap)
332
+ uploadButtonPaddingHorizontal = sanitizeNonNegative(getMapInt(uploadStyle, "paddingHorizontal", uploadButtonPaddingHorizontal), uploadButtonPaddingHorizontal)
333
+ uploadButtonPaddingVertical = sanitizeNonNegative(getMapInt(uploadStyle, "paddingVertical", uploadButtonPaddingVertical), uploadButtonPaddingVertical)
334
+ }
335
+
336
+ private fun handleImagePickResult(activity: Activity, resultCode: Int, data: Intent?) {
337
+ if (pendingPromise == null) return
338
+ if (resultCode == Activity.RESULT_CANCELED) {
339
+ rejectPending(E_PICKER_CANCELLED, E_PICKER_CANCELLED_MSG)
340
+ return
341
+ }
342
+ val sourceUri = data?.data
343
+ if (resultCode != Activity.RESULT_OK || sourceUri == null) {
344
+ rejectPending(E_NO_IMAGE_DATA_FOUND, "Cannot resolve image url")
345
+ return
346
+ }
347
+ startCrop(activity, sourceUri)
348
+ }
349
+
350
+ private fun handleCameraResult(activity: Activity, resultCode: Int, data: Intent?) {
351
+ if (pendingPromise == null) return
352
+ if (resultCode == Activity.RESULT_CANCELED) {
353
+ cleanupCameraUriPermissions(activity)
354
+ pendingCameraUri = null
355
+ rejectPending(E_PICKER_CANCELLED, E_PICKER_CANCELLED_MSG)
356
+ return
357
+ }
358
+ val sourceUri = pendingCameraUri ?: data?.data
359
+ cleanupCameraUriPermissions(activity)
360
+ pendingCameraUri = null
361
+ if (resultCode != Activity.RESULT_OK || sourceUri == null) {
362
+ rejectPending(E_NO_IMAGE_DATA_FOUND, "Cannot resolve image url")
363
+ return
364
+ }
365
+ startCrop(activity, sourceUri)
366
+ }
367
+
368
+ private fun startCrop(activity: Activity, sourceUri: Uri) {
369
+ try {
370
+ val ext = when (compressFormat) {
371
+ "png" -> "png"
372
+ "webp" -> "webp"
373
+ else -> "jpg"
374
+ }
375
+ val destinationFile = File(activity.cacheDir, "native-cropped-${UUID.randomUUID()}.$ext")
376
+ val destinationUri = Uri.fromFile(destinationFile)
377
+
378
+ val cropOptions = UCrop.Options().apply {
379
+ val fmt = when (compressFormat) {
380
+ "png" -> Bitmap.CompressFormat.PNG
381
+ "webp" -> {
382
+ if (android.os.Build.VERSION.SDK_INT >= 30) {
383
+ Bitmap.CompressFormat.WEBP_LOSSY
384
+ } else {
385
+ @Suppress("DEPRECATION")
386
+ Bitmap.CompressFormat.WEBP
387
+ }
388
+ }
389
+ else -> Bitmap.CompressFormat.JPEG
390
+ }
391
+ setCompressionFormat(fmt)
392
+ // PNG ignores quality but uCrop API requires an int.
393
+ setCompressionQuality(compressQuality.coerceIn(0, 100))
394
+ setToolbarTitle(headerTitle.ifBlank { "Preview Image" })
395
+ setToolbarColor(if (isDarkTheme) Color.parseColor("#121212") else Color.parseColor("#FFFFFF"))
396
+ setStatusBarColor(parseColor(statusBarColor, Color.WHITE))
397
+ setToolbarWidgetColor(if (isDarkTheme) Color.parseColor("#FFFFFF") else Color.parseColor("#111111"))
398
+ setShowCropGrid(cropGridEnabled)
399
+ setShowCropFrame(true)
400
+ if (cropFrameColor.isNotBlank()) setCropFrameColor(parseColor(cropFrameColor, Color.WHITE))
401
+ if (cropGridColor.isNotBlank()) setCropGridColor(parseColor(cropGridColor, Color.WHITE))
402
+ setDimmedLayerColor(parseColor(dimmedLayerColor, Color.parseColor("#B3000000")))
403
+ setCircleDimmedLayer(circularCrop)
404
+ setHideBottomControls(!showNativeCropControls)
405
+ setFreeStyleCropEnabled(freeStyleCropEnabled)
406
+ }
407
+
408
+ val cropIntent = UCrop.of(sourceUri, destinationUri)
409
+ .withOptions(cropOptions)
410
+ .withAspectRatio(cropWidth.toFloat(), cropHeight.toFloat())
411
+ .getIntent(activity)
412
+ .apply {
413
+ setClass(activity, NativeImageCropperActivity::class.java)
414
+ putExtra(NativeImageCropperActivity.EXTRA_HEADER_TITLE, headerTitle.ifBlank { "Preview Image" })
415
+ putExtra(NativeImageCropperActivity.EXTRA_CANCEL_TEXT, cancelText.ifBlank { "Cancel" })
416
+ putExtra(NativeImageCropperActivity.EXTRA_UPLOAD_TEXT, uploadText.ifBlank { "Upload" })
417
+ putExtra(NativeImageCropperActivity.EXTRA_CANCEL_COLOR, cancelColor.ifBlank { "#111111" })
418
+ putExtra(NativeImageCropperActivity.EXTRA_UPLOAD_COLOR, uploadColor.ifBlank { "#111111" })
419
+ putExtra(NativeImageCropperActivity.EXTRA_STATUS_BAR_COLOR, statusBarColor.ifBlank { "#FFFFFF" })
420
+ putExtra(NativeImageCropperActivity.EXTRA_IS_DARK_THEME, isDarkTheme)
421
+ putExtra(NativeImageCropperActivity.EXTRA_DRAW_UNDER_STATUS_BAR, drawUnderStatusBar)
422
+ putExtra(NativeImageCropperActivity.EXTRA_STATUS_BAR_STYLE, statusBarStyle)
423
+
424
+ putExtra(NativeImageCropperActivity.EXTRA_HEADER_BACKGROUND_COLOR, headerBackgroundColor)
425
+ putExtra(NativeImageCropperActivity.EXTRA_HEADER_TITLE_COLOR, headerTitleColor)
426
+ putExtra(NativeImageCropperActivity.EXTRA_HEADER_TITLE_FONT_SIZE, headerTitleFontSize)
427
+ putExtra(NativeImageCropperActivity.EXTRA_HEADER_TITLE_FONT_FAMILY, headerTitleFontFamily)
428
+ putExtra(NativeImageCropperActivity.EXTRA_HEADER_HEIGHT, headerHeight)
429
+ putExtra(NativeImageCropperActivity.EXTRA_HEADER_PADDING_HORIZONTAL, headerPaddingHorizontal)
430
+ putExtra(NativeImageCropperActivity.EXTRA_HEADER_PADDING_TOP, headerPaddingTop)
431
+ putExtra(NativeImageCropperActivity.EXTRA_HEADER_PADDING_BOTTOM, headerPaddingBottom)
432
+ putExtra(NativeImageCropperActivity.EXTRA_HEADER_ALIGNMENT, headerAlignment)
433
+
434
+ putExtra(NativeImageCropperActivity.EXTRA_BOTTOM_BACKGROUND_COLOR, buttonContainerBackgroundColor)
435
+ putExtra(NativeImageCropperActivity.EXTRA_BOTTOM_PADDING_HORIZONTAL, buttonContainerPaddingHorizontal)
436
+ putExtra(NativeImageCropperActivity.EXTRA_BOTTOM_PADDING_TOP, buttonContainerPaddingTop)
437
+ putExtra(NativeImageCropperActivity.EXTRA_BOTTOM_PADDING_BOTTOM, buttonContainerPaddingBottom)
438
+ putExtra(NativeImageCropperActivity.EXTRA_BOTTOM_BUTTON_GAP, buttonGap)
439
+ putExtra(NativeImageCropperActivity.EXTRA_BOTTOM_BUTTON_HEIGHT, buttonHeight)
440
+ putExtra(NativeImageCropperActivity.EXTRA_BOTTOM_BUTTON_LAYOUT, buttonLayout)
441
+ putExtra(NativeImageCropperActivity.EXTRA_CONTROLS_PLACEMENT, controlsPlacement)
442
+ putExtra(NativeImageCropperActivity.EXTRA_TOP_LEFT_CONTROL, topLeftControl)
443
+ putExtra(NativeImageCropperActivity.EXTRA_TOP_RIGHT_CONTROL, topRightControl)
444
+ putExtra(NativeImageCropperActivity.EXTRA_FOOTER_BUTTON_ORDER, footerButtonOrder)
445
+
446
+ putExtra(NativeImageCropperActivity.EXTRA_CANCEL_BACKGROUND_COLOR, cancelButtonBackgroundColor)
447
+ putExtra(NativeImageCropperActivity.EXTRA_CANCEL_BORDER_COLOR, cancelButtonBorderColor)
448
+ putExtra(NativeImageCropperActivity.EXTRA_CANCEL_BORDER_WIDTH, cancelButtonBorderWidth)
449
+ putExtra(NativeImageCropperActivity.EXTRA_CANCEL_FONT_SIZE, cancelButtonFontSize)
450
+ putExtra(NativeImageCropperActivity.EXTRA_CANCEL_FONT_FAMILY, cancelButtonFontFamily)
451
+ putExtra(NativeImageCropperActivity.EXTRA_CANCEL_BORDER_RADIUS, cancelButtonRadius)
452
+ putExtra(NativeImageCropperActivity.EXTRA_CANCEL_BUTTON_CONTENT, cancelButtonContent)
453
+ putExtra(NativeImageCropperActivity.EXTRA_CANCEL_BUTTON_ICON_URI, cancelButtonIconUri)
454
+ putExtra(NativeImageCropperActivity.EXTRA_CANCEL_BUTTON_ICON_BASE64, cancelButtonIconBase64)
455
+ putExtra(NativeImageCropperActivity.EXTRA_CANCEL_BUTTON_ICON_TINT, cancelButtonIconTintColor)
456
+ putExtra(NativeImageCropperActivity.EXTRA_CANCEL_BUTTON_ICON_SIZE, cancelButtonIconSize)
457
+ putExtra(NativeImageCropperActivity.EXTRA_CANCEL_BUTTON_ICON_GAP, cancelButtonIconGap)
458
+ putExtra(NativeImageCropperActivity.EXTRA_CANCEL_BUTTON_PADDING_HORIZONTAL, cancelButtonPaddingHorizontal)
459
+ putExtra(NativeImageCropperActivity.EXTRA_CANCEL_BUTTON_PADDING_VERTICAL, cancelButtonPaddingVertical)
460
+
461
+ putExtra(NativeImageCropperActivity.EXTRA_UPLOAD_TEXT_COLOR, uploadButtonTextColor)
462
+ putExtra(NativeImageCropperActivity.EXTRA_UPLOAD_COLOR, uploadButtonBackgroundColor)
463
+ putExtra(NativeImageCropperActivity.EXTRA_UPLOAD_BORDER_COLOR, uploadButtonBorderColor)
464
+ putExtra(NativeImageCropperActivity.EXTRA_UPLOAD_BORDER_WIDTH, uploadButtonBorderWidth)
465
+ putExtra(NativeImageCropperActivity.EXTRA_UPLOAD_FONT_SIZE, uploadButtonFontSize)
466
+ putExtra(NativeImageCropperActivity.EXTRA_UPLOAD_FONT_FAMILY, uploadButtonFontFamily)
467
+ putExtra(NativeImageCropperActivity.EXTRA_UPLOAD_BORDER_RADIUS, uploadButtonRadius)
468
+ putExtra(NativeImageCropperActivity.EXTRA_UPLOAD_BUTTON_CONTENT, uploadButtonContent)
469
+ putExtra(NativeImageCropperActivity.EXTRA_UPLOAD_BUTTON_ICON_URI, uploadButtonIconUri)
470
+ putExtra(NativeImageCropperActivity.EXTRA_UPLOAD_BUTTON_ICON_BASE64, uploadButtonIconBase64)
471
+ putExtra(NativeImageCropperActivity.EXTRA_UPLOAD_BUTTON_ICON_TINT, uploadButtonIconTintColor)
472
+ putExtra(NativeImageCropperActivity.EXTRA_UPLOAD_BUTTON_ICON_SIZE, uploadButtonIconSize)
473
+ putExtra(NativeImageCropperActivity.EXTRA_UPLOAD_BUTTON_ICON_GAP, uploadButtonIconGap)
474
+ putExtra(NativeImageCropperActivity.EXTRA_UPLOAD_BUTTON_PADDING_HORIZONTAL, uploadButtonPaddingHorizontal)
475
+ putExtra(NativeImageCropperActivity.EXTRA_UPLOAD_BUTTON_PADDING_VERTICAL, uploadButtonPaddingVertical)
476
+
477
+ putExtra(NativeImageCropperActivity.EXTRA_CROP_GRID_ENABLED, cropGridEnabled)
478
+ putExtra(NativeImageCropperActivity.EXTRA_CROP_FRAME_COLOR, cropFrameColor)
479
+ putExtra(NativeImageCropperActivity.EXTRA_CROP_GRID_COLOR, cropGridColor)
480
+ }
481
+
482
+ if (cropIntent.resolveActivity(activity.packageManager) == null) {
483
+ rejectPending(E_NO_APP_AVAILABLE, "Cropper activity is not available")
484
+ return
485
+ }
486
+ activity.startActivityForResult(cropIntent, UCrop.REQUEST_CROP)
487
+ activity.overridePendingTransition(0, 0)
488
+ } catch (error: Exception) {
489
+ rejectPending(E_PICKER_ERROR, error.message ?: "Unable to start cropper")
490
+ }
491
+ }
492
+
493
+ private fun launchCamera(activity: Activity) {
494
+ val granted =
495
+ ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) ==
496
+ android.content.pm.PackageManager.PERMISSION_GRANTED
497
+ if (!granted) {
498
+ rejectPending(E_PERMISSION_MISSING, "Camera permission is not granted")
499
+ return
500
+ }
501
+ val outputFile = File(activity.cacheDir, "native-camera-${UUID.randomUUID()}.jpg")
502
+ val authority = activity.packageName + ".nativeimagepicker.provider"
503
+ val outputUri = FileProvider.getUriForFile(activity, authority, outputFile)
504
+ pendingCameraUri = outputUri
505
+
506
+ val cameraIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
507
+ putExtra(MediaStore.EXTRA_OUTPUT, outputUri)
508
+ putExtra("android.intent.extra.quickCapture", true)
509
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
510
+ addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
511
+ }
512
+ val cameraHandlers = activity.packageManager.queryIntentActivities(cameraIntent, 0)
513
+ if (cameraHandlers.isNullOrEmpty()) {
514
+ pendingCameraUri = null
515
+ rejectPending(E_NO_APP_AVAILABLE, "No camera app available")
516
+ return
517
+ }
518
+ for (handler in cameraHandlers) {
519
+ activity.grantUriPermission(
520
+ handler.activityInfo.packageName,
521
+ outputUri,
522
+ Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
523
+ )
524
+ }
525
+ activity.startActivityForResult(cameraIntent, REQUEST_CAPTURE_IMAGE)
526
+ }
527
+
528
+ private fun launchSystemPhotoPicker(activity: Activity) {
529
+ val pickRequest = PickVisualMediaRequest.Builder()
530
+ .setMediaType(ActivityResultContracts.PickVisualMedia.ImageOnly)
531
+ .build()
532
+ val pickerIntent = ActivityResultContracts.PickVisualMedia().createIntent(activity, pickRequest).apply {
533
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
534
+ }
535
+ if (pickerIntent.resolveActivity(activity.packageManager) == null) {
536
+ rejectPending(E_NO_APP_AVAILABLE, "No gallery app available")
537
+ return
538
+ }
539
+ activity.startActivityForResult(pickerIntent, REQUEST_PICK_IMAGE)
540
+ }
541
+
542
+ private fun handleCropResult(resultCode: Int, data: Intent?) {
543
+ if (pendingPromise == null) return
544
+ if (resultCode == Activity.RESULT_CANCELED) {
545
+ rejectPending(E_PICKER_CANCELLED, E_PICKER_CANCELLED_MSG)
546
+ return
547
+ }
548
+ val resultUri = UCrop.getOutput(data ?: return rejectPending(E_NO_IMAGE_DATA_FOUND, "Cannot find image data"))
549
+ if (resultCode != Activity.RESULT_OK || resultUri == null) {
550
+ rejectPending(E_NO_IMAGE_DATA_FOUND, "Cannot find image data")
551
+ return
552
+ }
553
+
554
+ encodeExecutor.execute {
555
+ try {
556
+ if (isInvalidated || pendingPromise == null) return@execute
557
+ val result = WritableNativeMap().apply {
558
+ putString("path", resultUri.toString())
559
+ if (includeBase64) {
560
+ val encoded = toBase64String(resultUri)
561
+ if (encoded.isNullOrBlank()) {
562
+ rejectPending(E_NO_IMAGE_DATA_FOUND, "Cannot encode selected image")
563
+ return@execute
564
+ }
565
+ putString("base64", encoded)
566
+ }
567
+ }
568
+ resolvePending(result)
569
+ } catch (error: Exception) {
570
+ rejectPending(E_PICKER_ERROR, error.message ?: "Unable to process image")
571
+ } finally {
572
+ runCatching {
573
+ if ("file".equals(resultUri.scheme, ignoreCase = true)) {
574
+ File(resultUri.path.orEmpty()).delete()
575
+ }
576
+ }
577
+ }
578
+ }
579
+ }
580
+
581
+ private fun parseColor(color: String, fallback: Int): Int {
582
+ return runCatching { Color.parseColor(color) }.getOrElse { fallback }
583
+ }
584
+
585
+ private fun toBase64String(uri: Uri): String? {
586
+ emitProgress(0.0)
587
+ val output = ByteArrayOutputStream()
588
+ Base64OutputStream(output, Base64.NO_WRAP).use { base64Stream ->
589
+ val resolver = reactContext.contentResolver
590
+ val total = runCatching {
591
+ resolver.openAssetFileDescriptor(uri, "r")?.use { afd ->
592
+ val len = afd.length
593
+ if (len > 0) len else -1L
594
+ } ?: -1L
595
+ }.getOrDefault(-1L)
596
+ var readSoFar = 0L
597
+ var lastEmitted = 0.0
598
+ resolver.openInputStream(uri)?.use { input ->
599
+ val buffer = ByteArray(16 * 1024)
600
+ while (true) {
601
+ val read = input.read(buffer)
602
+ if (read <= 0) break
603
+ base64Stream.write(buffer, 0, read)
604
+ if (total > 0) {
605
+ readSoFar += read.toLong()
606
+ val p = (readSoFar.toDouble() / total.toDouble()).coerceIn(0.0, 1.0)
607
+ // Emit only if progress moved enough to matter.
608
+ if (p - lastEmitted >= 0.03) {
609
+ lastEmitted = p
610
+ emitProgress(p)
611
+ }
612
+ }
613
+ }
614
+ } ?: return null
615
+ }
616
+ emitProgress(1.0)
617
+ return output.toString(Charsets.UTF_8.name())
618
+ }
619
+
620
+ private fun emitProgress(progress: Double) {
621
+ if (isInvalidated) return
622
+ val map = WritableNativeMap().apply {
623
+ putDouble("progress", progress.coerceIn(0.0, 1.0))
624
+ }
625
+ reactContext
626
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
627
+ .emit("NativeImageCropperProgress", map)
628
+ }
629
+
630
+ private fun cleanupCameraUriPermissions(activity: Activity) {
631
+ val uri = pendingCameraUri ?: return
632
+ runCatching {
633
+ activity.revokeUriPermission(
634
+ uri,
635
+ Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
636
+ )
637
+ }
638
+ }
639
+
640
+ private fun resolvePending(data: WritableNativeMap) {
641
+ val promise = pendingPromise ?: return
642
+ pendingPromise = null
643
+ reactContext.runOnUiQueueThread { promise.resolve(data) }
644
+ }
645
+
646
+ private fun rejectPending(code: String, message: String) {
647
+ val promise = pendingPromise ?: return
648
+ pendingPromise = null
649
+ reactContext.runOnUiQueueThread { promise.reject(code, message) }
650
+ }
651
+
652
+ private fun ReadableMap.getIntOrDefault(key: String, fallback: Int): Int {
653
+ if (!hasKey(key) || isNull(key)) return fallback
654
+ return runCatching { getInt(key) }.getOrDefault(fallback)
655
+ }
656
+
657
+ private fun ReadableMap.getStringOrDefault(key: String, fallback: String): String {
658
+ if (!hasKey(key) || isNull(key)) return fallback
659
+ return getString(key).orEmpty().ifBlank { fallback }
660
+ }
661
+
662
+ private fun sanitizePositive(value: Int, fallback: Int): Int {
663
+ return if (value > 0) value else fallback
664
+ }
665
+
666
+ private fun sanitizeNonNegative(value: Int, fallback: Int): Int {
667
+ return if (value >= 0) value else fallback
668
+ }
669
+
670
+ private fun getOptionalMap(map: ReadableMap, key: String): ReadableMap? {
671
+ return if (map.hasKey(key) && !map.isNull(key)) map.getMap(key) else null
672
+ }
673
+
674
+ private fun getMapString(map: ReadableMap?, key: String, fallback: String): String {
675
+ if (map == null || !map.hasKey(key) || map.isNull(key)) return fallback
676
+ return map.getString(key).orEmpty().ifBlank { fallback }
677
+ }
678
+
679
+ private fun getMapInt(map: ReadableMap?, key: String, fallback: Int): Int {
680
+ if (map == null || !map.hasKey(key) || map.isNull(key)) return fallback
681
+ return runCatching { map.getInt(key) }.getOrDefault(fallback)
682
+ }
683
+ }
684
+