@onekeyfe/react-native-image-crop-picker 3.0.147 → 3.0.148

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 (34) hide show
  1. package/README.md +48 -5
  2. package/ReactNativeImageCropPicker.podspec +5 -3
  3. package/android/src/main/AndroidManifest.xml +2 -2
  4. package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ImageCropPickerImageProcessor.kt +8 -18
  5. package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ImageCropPickerSession.kt +21 -46
  6. package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ImageCropperActivity.kt +636 -0
  7. package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ImageCropperTheme.kt +188 -0
  8. package/android/src/main/res/drawable/image_crop_picker_rotate.xml +11 -0
  9. package/ios/ImageCropPickerImageProcessor.swift +8 -4
  10. package/ios/ImageCropPickerSession.swift +43 -46
  11. package/ios/ImageCropperTheme.swift +155 -0
  12. package/ios/ImageCropperViewController.swift +462 -0
  13. package/ios/TOCropViewController/Views/TOCropOverlayView.h +9 -0
  14. package/ios/TOCropViewController/Views/TOCropOverlayView.m +47 -3
  15. package/lib/typescript/src/ReactNativeImageCropPicker.nitro.d.ts +17 -11
  16. package/lib/typescript/src/ReactNativeImageCropPicker.nitro.d.ts.map +1 -1
  17. package/nitrogen/generated/android/c++/JHybridReactNativeImageCropPickerSpec.cpp +8 -0
  18. package/nitrogen/generated/android/c++/JImageCropPickerOptions.hpp +9 -45
  19. package/nitrogen/generated/android/c++/JImageCropperAppearance.hpp +108 -0
  20. package/nitrogen/generated/android/c++/JImageCropperColorScheme.hpp +58 -0
  21. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/ImageCropPickerOptions.kt +5 -55
  22. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/ImageCropperAppearance.kt +111 -0
  23. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/ImageCropperColorScheme.kt +23 -0
  24. package/nitrogen/generated/ios/ReactNativeImageCropPicker-Swift-Cxx-Bridge.hpp +36 -0
  25. package/nitrogen/generated/ios/ReactNativeImageCropPicker-Swift-Cxx-Umbrella.hpp +6 -0
  26. package/nitrogen/generated/ios/c++/HybridReactNativeImageCropPickerSpecSwift.hpp +6 -0
  27. package/nitrogen/generated/ios/swift/ImageCropPickerOptions.swift +6 -193
  28. package/nitrogen/generated/ios/swift/ImageCropperAppearance.swift +251 -0
  29. package/nitrogen/generated/ios/swift/ImageCropperColorScheme.swift +40 -0
  30. package/nitrogen/generated/shared/c++/ImageCropPickerOptions.hpp +8 -46
  31. package/nitrogen/generated/shared/c++/ImageCropperAppearance.hpp +134 -0
  32. package/nitrogen/generated/shared/c++/ImageCropperColorScheme.hpp +76 -0
  33. package/package.json +1 -1
  34. package/src/ReactNativeImageCropPicker.nitro.ts +29 -13
package/README.md CHANGED
@@ -4,16 +4,18 @@ Single photo picker and cropper for OneKey, built on [Nitro Modules](https://nit
4
4
 
5
5
  Neither platform asks for photo library permission:
6
6
 
7
- - **iOS** picks with `PHPickerViewController`, which runs out of process. `react-native-image-crop-picker` requested full photo library access first. Once a user denied it, iOS never showed the prompt again and every later `openPicker` call failed silently (OK-48227). Cropping uses a vendored TOCropViewController 3.2.0 with the OK-51551 rotation fix, which upstream still lacks.
8
- - **Android** picks with the system Photo Picker (`ActivityResultContracts.PickVisualMedia`, which falls back to `ACTION_OPEN_DOCUMENT` on devices without it) and crops with uCrop `2.2.11-native`. Activity results go through the activity's `ActivityResultRegistry`, so no `ActivityEventListener` is needed.
7
+ - **iOS** picks with `PHPickerViewController`, which runs out of process. `react-native-image-crop-picker` requested full photo library access first. Once a user denied it, iOS never showed the prompt again and every later `openPicker` call failed silently (OK-48227). The crop gestures come from a vendored TOCropViewController 3.2.0 `TOCropView`, with the OK-51551 rotation fix that upstream still lacks.
8
+ - **Android** picks with the system Photo Picker (`ActivityResultContracts.PickVisualMedia`, which falls back to `ACTION_OPEN_DOCUMENT` on devices without it). The crop gestures come from uCrop `2.2.11-native`'s `UCropView`. Activity results go through the activity's `ActivityResultRegistry`, so no `ActivityEventListener` is needed.
9
+
10
+ Both platforms show the same cropper screen, drawn by this package rather than by TOCropViewController or uCrop. See [Cropper screen](#cropper-screen).
9
11
 
10
12
  ## Installation
11
13
 
12
14
  ```sh
13
- yarn add react-native-image-crop-picker@npm:@onekeyfe/react-native-image-crop-picker react-native-nitro-modules
15
+ yarn add @onekeyfe/react-native-image-crop-picker react-native-nitro-modules
14
16
  ```
15
17
 
16
- The npm alias keeps existing `react-native-image-crop-picker` imports working.
18
+ To keep existing `react-native-image-crop-picker` imports working, install it under that name with an npm alias instead: `react-native-image-crop-picker@npm:@onekeyfe/react-native-image-crop-picker`.
17
19
 
18
20
  - **iOS**: do not also install the `TOCropViewController` pod. This package compiles its own copy, and the duplicate classes would clash at link time.
19
21
  - **Android**: uCrop is only published to JitPack. Add `maven { url "https://www.jitpack.io" }` to the app's `allprojects.repositories`.
@@ -23,7 +25,7 @@ The npm alias keeps existing `react-native-image-crop-picker` imports working.
23
25
  ```ts
24
26
  import ImageCropPicker, {
25
27
  ImageCropPickerError,
26
- } from 'react-native-image-crop-picker';
28
+ } from '@onekeyfe/react-native-image-crop-picker';
27
29
 
28
30
  try {
29
31
  const image = await ImageCropPicker.openPicker({
@@ -50,6 +52,46 @@ const cropped = await ImageCropPicker.openCropper({
50
52
 
51
53
  `openCropper` accepts `file://` URIs, absolute paths, `http(s)://` URLs, `data:` URIs and, on Android, `content://` URIs.
52
54
 
55
+ ## Cropper screen
56
+
57
+ The cropper is a full-screen page with the same layout and metrics on iOS and Android:
58
+
59
+ - A 56 pt header with `cropperToolbarTitle` centered and a rotate button on the right. The rotate button turns the image 90° counterclockwise; `cropperRotateButtonsHidden` hides it.
60
+ - The crop area. The crop box keeps 20 pt from every edge, and the image outside it shows the page background at 70% opacity. The rule-of-thirds grid shows while the image is moved, unless `showCropGuidelines` is `false`.
61
+ - A footer with two capsule buttons, `cropperCancelText` and `cropperChooseText`, 50 pt tall with 10 pt between them. The confirm button shows a spinner while the image is saved.
62
+
63
+ `cropperAppearance` sets its colors and fonts:
64
+
65
+ ```ts
66
+ await ImageCropPicker.openPicker({
67
+ width: 240,
68
+ height: 240,
69
+ cropping: true,
70
+ cropperToolbarTitle: 'Crop image',
71
+ cropperAppearance: {
72
+ colorScheme: 'dark',
73
+ backgroundColor: '#0f0f0f',
74
+ confirmButtonColor: '#ffffffed',
75
+ confirmButtonTextColor: '#000000df',
76
+ titleFontFamily: 'Roobert-SemiBold',
77
+ buttonFontFamily: 'Roobert-Medium',
78
+ },
79
+ });
80
+ ```
81
+
82
+ | Field | Used for |
83
+ | --- | --- |
84
+ | `colorScheme` | `'light'` or `'dark'`. Picks the fallback palette and the status bar style. Defaults to the system appearance. |
85
+ | `backgroundColor` | The page. The area outside the crop box is this color at 70% opacity. |
86
+ | `titleColor` | The title and the crop box border. |
87
+ | `iconColor` | The rotate button. |
88
+ | `cancelButtonColor`, `cancelButtonPressedColor`, `cancelButtonTextColor` | The cancel button. |
89
+ | `confirmButtonColor`, `confirmButtonPressedColor`, `confirmButtonTextColor` | The confirm button. |
90
+ | `titleFontFamily`, `buttonFontFamily` | Fonts bundled with the app. iOS looks them up with `UIFont(name:)`, Android with React Native's `ReactFontManager`. |
91
+ | `scale` | Multiplies every size, for apps that scale their UI. |
92
+
93
+ Colors are CSS hex strings: `#RGB`, `#RRGGBB` or `#RRGGBBAA`. Anything left out falls back to OneKey's light or dark palette.
94
+
53
95
  ## Behavior
54
96
 
55
97
  - Results are always JPEG. `width` and `height` set the crop aspect ratio, and the cropped image is scaled to exactly that size, so a crop box a pixel off the ratio still yields the requested dimensions. With `freeStyleCropEnabled`, the crop keeps its own aspect ratio and is scaled to fit inside `width` × `height`.
@@ -58,6 +100,7 @@ const cropped = await ImageCropPicker.openCropper({
58
100
  - Results are written to `<tmp>/react-native-image-crop-picker/` on iOS and `<cache>/react-native-image-crop-picker/` on Android. `clean()` empties that directory.
59
101
  - Only one picker or cropper can be open at a time. A second call rejects with `E_PICKER_IN_PROGRESS`.
60
102
  - On iOS, cancelling the cropper that `openPicker` opened returns to the photo picker. On Android it rejects with `E_PICKER_CANCELLED`.
103
+ - On Android the cropper keeps the calling activity's requested orientation, so a portrait-only app gets a portrait-only cropper.
61
104
 
62
105
  ## Not supported
63
106
 
@@ -19,9 +19,11 @@ Pod::Spec.new do |s|
19
19
  "cpp/**/*.{hpp,cpp}",
20
20
  ]
21
21
 
22
- # Vendored TOCropViewController 3.2.0 with the OK-51551 rotation fix. Its
23
- # headers are public so the Swift sources can use it. It replaces the
24
- # standalone TOCropViewController pod, which must not be installed alongside.
22
+ # Vendored TOCropViewController 3.2.0 with the OK-51551 rotation fix, plus
23
+ # color hooks on TOCropOverlayView. Only its TOCropView is used, hosted by
24
+ # ImageCropperViewController. Its headers are public so the Swift sources can
25
+ # use it. It replaces the standalone TOCropViewController pod, which must not
26
+ # be installed alongside.
25
27
  s.public_header_files = ["ios/TOCropViewController/**/*.h"]
26
28
  # TOCropViewController looks up its strings in a bundle with exactly this name.
27
29
  s.resource_bundles = {
@@ -3,9 +3,9 @@
3
3
 
4
4
  <application>
5
5
  <activity
6
- android:name="com.yalantis.ucrop.UCropActivity"
6
+ android:name="com.margelo.nitro.reactnativeimagecroppicker.ImageCropperActivity"
7
7
  android:exported="false"
8
- android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
8
+ android:theme="@style/Theme.AppCompat.NoActionBar" />
9
9
 
10
10
  <!-- Prompts Google Play services to install the backported photo picker module. -->
11
11
  <service
@@ -28,16 +28,11 @@ internal data class ImageCropPickerConfig(
28
28
  val freeStyleCropEnabled: Boolean,
29
29
  val cropperCircleOverlay: Boolean,
30
30
  val cropperToolbarTitle: String?,
31
- val cropperActiveWidgetColor: String?,
32
- val cropperToolbarColor: String?,
33
- val cropperToolbarWidgetColor: String?,
34
- val cropperStatusBarLight: Boolean,
35
- val cropperNavigationBarLight: Boolean,
31
+ val cropperChooseText: String?,
32
+ val cropperCancelText: String?,
33
+ val cropperRotateButtonsHidden: Boolean,
36
34
  val showCropGuidelines: Boolean,
37
- val showCropFrame: Boolean,
38
- val enableRotationGesture: Boolean,
39
- val hideBottomControls: Boolean,
40
- val disableCropperColorSetters: Boolean,
35
+ val appearance: ImageCropperAppearance?,
41
36
  ) {
42
37
  companion object {
43
38
  fun from(options: ImageCropPickerOptions, forceCropping: Boolean) = ImageCropPickerConfig(
@@ -51,16 +46,11 @@ internal data class ImageCropPickerConfig(
51
46
  freeStyleCropEnabled = options.freeStyleCropEnabled == true,
52
47
  cropperCircleOverlay = options.cropperCircleOverlay == true,
53
48
  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,
49
+ cropperChooseText = options.cropperChooseText,
50
+ cropperCancelText = options.cropperCancelText,
51
+ cropperRotateButtonsHidden = options.cropperRotateButtonsHidden == true,
59
52
  showCropGuidelines = options.showCropGuidelines ?: true,
60
- showCropFrame = options.showCropFrame ?: true,
61
- enableRotationGesture = options.enableRotationGesture == true,
62
- hideBottomControls = options.hideBottomControls == true,
63
- disableCropperColorSetters = options.disableCropperColorSetters == true,
53
+ appearance = options.cropperAppearance,
64
54
  )
65
55
  }
66
56
  }
@@ -1,8 +1,7 @@
1
1
  package com.margelo.nitro.reactnativeimagecroppicker
2
2
 
3
3
  import android.app.Activity
4
- import android.graphics.Bitmap
5
- import android.graphics.Color
4
+ import android.content.res.Configuration
6
5
  import android.net.Uri
7
6
  import android.os.Handler
8
7
  import android.os.Looper
@@ -15,8 +14,6 @@ import androidx.activity.result.contract.ActivityResultContract
15
14
  import androidx.activity.result.contract.ActivityResultContracts
16
15
  import com.margelo.nitro.core.Promise
17
16
  import com.margelo.nitro.nativelogger.OneKeyLog
18
- import com.yalantis.ucrop.UCrop
19
- import com.yalantis.ucrop.UCropActivity
20
17
  import java.io.File
21
18
  import java.util.UUID
22
19
  import java.util.concurrent.ExecutorService
@@ -124,30 +121,37 @@ internal class ImageCropPickerSession(
124
121
  }
125
122
  temporaryFiles.add(destination)
126
123
 
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
- }
124
+ val systemIsDark = (activity.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) ==
125
+ Configuration.UI_MODE_NIGHT_YES
126
+ val intent = ImageCropperActivity.createIntent(
127
+ activity,
128
+ source,
129
+ Uri.fromFile(destination),
130
+ config,
131
+ ImageCropperTheme.resolve(config.appearance, systemIsDark),
132
+ // Keeps the cropper in the app's orientation, e.g. portrait on phones.
133
+ activity.requestedOrientation,
134
+ )
131
135
  val launcher = register(ActivityResultContracts.StartActivityForResult()) { result ->
132
136
  handleCropResult(result)
133
137
  }
134
- launch { launcher.launch(cropper.getIntent(activity)) }
138
+ launch { launcher.launch(intent) }
135
139
  }
136
140
 
137
141
  private fun handleCropResult(result: ActivityResult) {
138
142
  val data = result.data
139
143
  when (result.resultCode) {
140
144
  Activity.RESULT_OK -> {
141
- val output = data?.let { UCrop.getOutput(it) }
145
+ val output = data?.let { ImageCropperActivity.getOutputUri(it) }
142
146
  if (data == null || output == null) {
143
147
  finish(Result.failure(ImageCropPickerException.noImageData()))
144
148
  return
145
149
  }
146
150
  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
+ x = data.getIntExtra(ImageCropperActivity.EXTRA_OUTPUT_OFFSET_X, -1).toDouble(),
152
+ y = data.getIntExtra(ImageCropperActivity.EXTRA_OUTPUT_OFFSET_Y, -1).toDouble(),
153
+ width = data.getIntExtra(ImageCropperActivity.EXTRA_OUTPUT_WIDTH, -1).toDouble(),
154
+ height = data.getIntExtra(ImageCropperActivity.EXTRA_OUTPUT_HEIGHT, -1).toDouble(),
151
155
  )
152
156
  val filename = pickedFilename
153
157
  runInBackground(
@@ -164,35 +168,15 @@ internal class ImageCropPickerSession(
164
168
  onComplete = ::finish,
165
169
  )
166
170
  }
167
- UCrop.RESULT_ERROR -> {
168
- val message = data?.let { UCrop.getError(it)?.message } ?: "Cannot crop image"
171
+ ImageCropperActivity.RESULT_ERROR -> {
172
+ val message = data?.getStringExtra(ImageCropperActivity.EXTRA_ERROR_MESSAGE)
173
+ ?: "Cannot crop image"
169
174
  finish(Result.failure(ImageCropPickerException.noImageData(message)))
170
175
  }
171
176
  else -> finish(Result.failure(ImageCropPickerException.cancelled()))
172
177
  }
173
178
  }
174
179
 
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
180
  private fun <I, O> register(
197
181
  contract: ActivityResultContract<I, O>,
198
182
  callback: (O) -> Unit,
@@ -280,15 +264,6 @@ internal class ImageCropPickerSession(
280
264
  return displayName ?: uri.lastPathSegment
281
265
  }
282
266
 
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
267
  companion object {
293
268
  private const val TAG = "ImageCropPicker"
294
269
  private val mainHandler = Handler(Looper.getMainLooper())