react-native-expo-cropper 1.2.39 → 1.2.41
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/dist/CustomCamera.js +1 -1
- package/dist/ImageCropper.js +61 -24
- package/package.json +1 -1
- package/src/CustomCamera.js +20 -20
- package/src/ImageCropper.js +31 -17
package/dist/CustomCamera.js
CHANGED
|
@@ -97,7 +97,7 @@ function CustomCamera(_ref) {
|
|
|
97
97
|
// ✅ CRITICAL FIX: Calculate green frame coordinates relative to camera preview
|
|
98
98
|
// The green frame should be calculated on the wrapper (as it's visually drawn there)
|
|
99
99
|
// But we store it with wrapper dimensions so ImageCropper can map it correctly
|
|
100
|
-
//
|
|
100
|
+
//
|
|
101
101
|
// NOTE: Camera capture aspect ratio (typically 4:3) may differ from wrapper aspect ratio (9:16)
|
|
102
102
|
// This is handled in ImageCropper by using "cover" mode to match preview content
|
|
103
103
|
var calculateGreenFrameCoordinates = function calculateGreenFrameCoordinates() {
|
package/dist/ImageCropper.js
CHANGED
|
@@ -35,12 +35,14 @@ function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r)
|
|
|
35
35
|
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
|
|
36
36
|
function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t["return"] && (u = t["return"](), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
|
|
37
37
|
function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
|
|
38
|
+
var PRIMARY_GREEN = '#198754';
|
|
38
39
|
var ImageCropper = function ImageCropper(_ref) {
|
|
39
40
|
var _cameraFrameData$curr3;
|
|
40
41
|
var onConfirm = _ref.onConfirm,
|
|
41
42
|
openCameraFirst = _ref.openCameraFirst,
|
|
42
43
|
initialImage = _ref.initialImage,
|
|
43
|
-
addheight = _ref.addheight
|
|
44
|
+
addheight = _ref.addheight,
|
|
45
|
+
rotationLabel = _ref.rotationLabel;
|
|
44
46
|
var _useState = (0, _react.useState)(null),
|
|
45
47
|
_useState2 = _slicedToArray(_useState, 2),
|
|
46
48
|
image = _useState2[0],
|
|
@@ -242,6 +244,7 @@ var ImageCropper = function ImageCropper(_ref) {
|
|
|
242
244
|
_useState20 = _slicedToArray(_useState19, 2),
|
|
243
245
|
isRotating = _useState20[0],
|
|
244
246
|
setIsRotating = _useState20[1];
|
|
247
|
+
var rotationInProgressRef = (0, _react.useRef)(false); // block duplicate taps immediately
|
|
245
248
|
var lastValidPosition = (0, _react.useRef)(null);
|
|
246
249
|
var insets = (0, _reactNativeSafeAreaContext.useSafeAreaInsets)();
|
|
247
250
|
|
|
@@ -249,7 +252,7 @@ var ImageCropper = function ImageCropper(_ref) {
|
|
|
249
252
|
|
|
250
253
|
// No view-shot / captureRef / bitmap masking on device.
|
|
251
254
|
var enableMask = false;
|
|
252
|
-
var enableRotation =
|
|
255
|
+
var enableRotation = true; // rotation resets crop box so it is re-initialized on rotated image.
|
|
253
256
|
|
|
254
257
|
(0, _react.useEffect)(function () {
|
|
255
258
|
if (openCameraFirst) {
|
|
@@ -1212,45 +1215,54 @@ var ImageCropper = function ImageCropper(_ref) {
|
|
|
1212
1215
|
return _regenerator().w(function (_context) {
|
|
1213
1216
|
while (1) switch (_context.p = _context.n) {
|
|
1214
1217
|
case 0:
|
|
1215
|
-
if (
|
|
1218
|
+
if (image) {
|
|
1216
1219
|
_context.n = 1;
|
|
1217
1220
|
break;
|
|
1218
1221
|
}
|
|
1219
1222
|
return _context.a(2);
|
|
1220
1223
|
case 1:
|
|
1224
|
+
if (!rotationInProgressRef.current) {
|
|
1225
|
+
_context.n = 2;
|
|
1226
|
+
break;
|
|
1227
|
+
}
|
|
1228
|
+
return _context.a(2);
|
|
1229
|
+
case 2:
|
|
1230
|
+
// block duplicate taps immediately (no re-render delay)
|
|
1231
|
+
rotationInProgressRef.current = true;
|
|
1221
1232
|
setIsRotating(true);
|
|
1222
|
-
_context.p =
|
|
1223
|
-
// ✅ CORRECTION : appliquer la rotation de façon incrémentale sur le fichier (pas cumulée sur un fichier déjà roté).
|
|
1224
|
-
// L'ancienne version rotait l'image déjà rotée par l'angle total, ce qui donnait des rotations incorrectes (90 + 180 => 270).
|
|
1233
|
+
_context.p = 3;
|
|
1225
1234
|
rotationAngle.current = (rotationAngle.current + degrees) % 360;
|
|
1226
|
-
|
|
1235
|
+
|
|
1236
|
+
// Use JPEG for preview rotation (faster than PNG for large images; quality 0.92 is fine for preview)
|
|
1237
|
+
_context.n = 4;
|
|
1227
1238
|
return ImageManipulator.manipulateAsync(image, [{
|
|
1228
1239
|
rotate: degrees
|
|
1229
1240
|
}], {
|
|
1230
|
-
compress:
|
|
1231
|
-
|
|
1232
|
-
format: ImageManipulator.SaveFormat.PNG // Format sans perte
|
|
1241
|
+
compress: 0.92,
|
|
1242
|
+
format: ImageManipulator.SaveFormat.JPEG
|
|
1233
1243
|
});
|
|
1234
|
-
case
|
|
1244
|
+
case 4:
|
|
1235
1245
|
rotated = _context.v;
|
|
1236
|
-
|
|
1246
|
+
setPoints([]);
|
|
1247
|
+
hasInitializedCropBox.current = false;
|
|
1237
1248
|
setImage(rotated.uri);
|
|
1238
|
-
console.log("Rotation applied
|
|
1239
|
-
_context.n =
|
|
1249
|
+
console.log("Rotation applied:", degrees, "degrees; accumulated:", rotationAngle.current);
|
|
1250
|
+
_context.n = 6;
|
|
1240
1251
|
break;
|
|
1241
|
-
case
|
|
1242
|
-
_context.p =
|
|
1252
|
+
case 5:
|
|
1253
|
+
_context.p = 5;
|
|
1243
1254
|
_t = _context.v;
|
|
1244
1255
|
console.error("Error rotating image:", _t);
|
|
1245
1256
|
alert("Error rotating image");
|
|
1246
|
-
case 5:
|
|
1247
|
-
_context.p = 5;
|
|
1248
|
-
setIsRotating(false);
|
|
1249
|
-
return _context.f(5);
|
|
1250
1257
|
case 6:
|
|
1258
|
+
_context.p = 6;
|
|
1259
|
+
rotationInProgressRef.current = false;
|
|
1260
|
+
setIsRotating(false);
|
|
1261
|
+
return _context.f(6);
|
|
1262
|
+
case 7:
|
|
1251
1263
|
return _context.a(2);
|
|
1252
1264
|
}
|
|
1253
|
-
}, _callee, null, [[
|
|
1265
|
+
}, _callee, null, [[3, 5, 6, 7]]);
|
|
1254
1266
|
}));
|
|
1255
1267
|
return function rotatePreviewImage(_x) {
|
|
1256
1268
|
return _ref2.apply(this, arguments);
|
|
@@ -1411,17 +1423,42 @@ var ImageCropper = function ImageCropper(_ref) {
|
|
|
1411
1423
|
fill: "white"
|
|
1412
1424
|
});
|
|
1413
1425
|
}));
|
|
1414
|
-
}()))
|
|
1426
|
+
}())), isRotating && /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
|
|
1427
|
+
style: {
|
|
1428
|
+
position: 'absolute',
|
|
1429
|
+
left: 0,
|
|
1430
|
+
right: 0,
|
|
1431
|
+
top: 0,
|
|
1432
|
+
bottom: 0,
|
|
1433
|
+
backgroundColor: 'rgba(0,0,0,0.6)',
|
|
1434
|
+
justifyContent: 'center',
|
|
1435
|
+
alignItems: 'center'
|
|
1436
|
+
}
|
|
1437
|
+
}, /*#__PURE__*/_react["default"].createElement(_reactNative.ActivityIndicator, {
|
|
1438
|
+
size: "large",
|
|
1439
|
+
color: PRIMARY_GREEN
|
|
1440
|
+
}), /*#__PURE__*/_react["default"].createElement(_reactNative.Text, {
|
|
1441
|
+
style: {
|
|
1442
|
+
color: PRIMARY_GREEN,
|
|
1443
|
+
marginTop: 8,
|
|
1444
|
+
fontSize: 14
|
|
1445
|
+
}
|
|
1446
|
+
}, rotationLabel !== null && rotationLabel !== void 0 ? rotationLabel : 'Rotation...'))), !showResult && image && /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
|
|
1415
1447
|
style: [_ImageCropperStyles["default"].buttonContainerBelow, {
|
|
1416
1448
|
paddingBottom: Math.max(insets.bottom, 16)
|
|
1417
1449
|
}]
|
|
1418
1450
|
}, _reactNative.Platform.OS === 'android' && /*#__PURE__*/_react["default"].createElement(_reactNative.TouchableOpacity, {
|
|
1419
|
-
style: _ImageCropperStyles["default"].rotationButton,
|
|
1451
|
+
style: [_ImageCropperStyles["default"].rotationButton, isRotating && {
|
|
1452
|
+
opacity: 0.7
|
|
1453
|
+
}],
|
|
1420
1454
|
onPress: function onPress() {
|
|
1421
1455
|
return enableRotation && rotatePreviewImage(90);
|
|
1422
1456
|
},
|
|
1423
1457
|
disabled: isRotating
|
|
1424
|
-
}, /*#__PURE__*/_react["default"].createElement(
|
|
1458
|
+
}, isRotating ? /*#__PURE__*/_react["default"].createElement(_reactNative.ActivityIndicator, {
|
|
1459
|
+
size: "small",
|
|
1460
|
+
color: "white"
|
|
1461
|
+
}) : /*#__PURE__*/_react["default"].createElement(_vectorIcons.Ionicons, {
|
|
1425
1462
|
name: "sync",
|
|
1426
1463
|
size: 24,
|
|
1427
1464
|
color: "white"
|
package/package.json
CHANGED
package/src/CustomCamera.js
CHANGED
|
@@ -52,24 +52,24 @@ export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
|
|
|
52
52
|
// ✅ CRITICAL FIX: Calculate green frame coordinates relative to camera preview
|
|
53
53
|
// The green frame should be calculated on the wrapper (as it's visually drawn there)
|
|
54
54
|
// But we store it with wrapper dimensions so ImageCropper can map it correctly
|
|
55
|
-
//
|
|
55
|
+
//
|
|
56
56
|
// NOTE: Camera capture aspect ratio (typically 4:3) may differ from wrapper aspect ratio (9:16)
|
|
57
57
|
// This is handled in ImageCropper by using "cover" mode to match preview content
|
|
58
58
|
const calculateGreenFrameCoordinates = () => {
|
|
59
59
|
const wrapperWidth = cameraWrapperLayout.width;
|
|
60
60
|
const wrapperHeight = cameraWrapperLayout.height;
|
|
61
|
-
|
|
61
|
+
|
|
62
62
|
if (wrapperWidth === 0 || wrapperHeight === 0) {
|
|
63
63
|
console.warn("Camera wrapper layout not ready, cannot calculate green frame");
|
|
64
64
|
return null;
|
|
65
65
|
}
|
|
66
|
-
|
|
66
|
+
|
|
67
67
|
// ✅ Calculate green frame as percentage of WRAPPER
|
|
68
68
|
const frameWidth = wrapperWidth * 0.85; // 85% of wrapper width
|
|
69
69
|
const frameHeight = wrapperHeight * 0.70; // 70% of wrapper height
|
|
70
70
|
const frameX = (wrapperWidth - frameWidth) / 2; // Centered horizontally
|
|
71
71
|
const frameY = (wrapperHeight - frameHeight) / 2; // Centered vertically
|
|
72
|
-
|
|
72
|
+
|
|
73
73
|
const frameCoords = {
|
|
74
74
|
x: frameX,
|
|
75
75
|
y: frameY,
|
|
@@ -83,7 +83,7 @@ export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
|
|
|
83
83
|
percentWidth: 85, // 85% of wrapper width
|
|
84
84
|
percentHeight: 70 // 70% of wrapper height
|
|
85
85
|
};
|
|
86
|
-
|
|
86
|
+
|
|
87
87
|
console.log("✅ Green frame coordinates calculated:", frameCoords);
|
|
88
88
|
return frameCoords;
|
|
89
89
|
};
|
|
@@ -104,26 +104,26 @@ export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
|
|
|
104
104
|
waitForRender(5).then(() => {
|
|
105
105
|
setLoadingBeforeCapture(true);
|
|
106
106
|
});
|
|
107
|
-
|
|
107
|
+
|
|
108
108
|
// Wait a bit before taking picture (works on iOS)
|
|
109
109
|
await waitForRender(2);
|
|
110
|
-
|
|
110
|
+
|
|
111
111
|
// ✅ OPTIMIZED: Capture with maximum quality and native camera ratio
|
|
112
112
|
// Platform-specific optimizations for best quality
|
|
113
113
|
const captureOptions = {
|
|
114
114
|
// Maximum quality (0-1, 1 = best quality, no compression)
|
|
115
115
|
quality: 1,
|
|
116
|
-
|
|
116
|
+
|
|
117
117
|
// Disable shutter sound for better UX
|
|
118
118
|
shutterSound: false,
|
|
119
|
-
|
|
119
|
+
|
|
120
120
|
// ✅ CRITICAL: skipProcessing preserves original resolution and avoids interpolation
|
|
121
121
|
// This ensures pixel-perfect quality and prevents premature resizing
|
|
122
122
|
skipProcessing: true,
|
|
123
|
-
|
|
123
|
+
|
|
124
124
|
// Include EXIF metadata (orientation, camera settings, etc.)
|
|
125
125
|
exif: true,
|
|
126
|
-
|
|
126
|
+
|
|
127
127
|
// ✅ Platform-specific optimizations
|
|
128
128
|
...(Platform.OS === 'ios' && {
|
|
129
129
|
// iOS: Use native capture format (typically 4:3 or 16:9 depending on device)
|
|
@@ -162,39 +162,39 @@ export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
|
|
|
162
162
|
// ✅ CRITICAL FIX: Use the same green frame coordinates that are used for rendering
|
|
163
163
|
// Fallback to recalculation if, for some reason, state is not yet set
|
|
164
164
|
const greenFrameCoords = greenFrame || calculateGreenFrameCoordinates();
|
|
165
|
-
|
|
165
|
+
|
|
166
166
|
if (!greenFrameCoords) {
|
|
167
167
|
throw new Error("Green frame coordinates not available");
|
|
168
168
|
}
|
|
169
|
-
|
|
169
|
+
|
|
170
170
|
// ✅ Send photo URI and frame data to ImageCropper
|
|
171
171
|
// The photo maintains its native resolution and aspect ratio
|
|
172
172
|
// ImageCropper will handle display and cropping while preserving quality
|
|
173
173
|
onPhotoCaptured(photo.uri, {
|
|
174
174
|
greenFrame: greenFrameCoords,
|
|
175
|
-
capturedImageSize: {
|
|
176
|
-
width: photo.width,
|
|
175
|
+
capturedImageSize: {
|
|
176
|
+
width: photo.width,
|
|
177
177
|
height: photo.height,
|
|
178
178
|
aspectRatio: capturedAspectRatio
|
|
179
179
|
}
|
|
180
180
|
});
|
|
181
|
-
|
|
181
|
+
|
|
182
182
|
setLoadingBeforeCapture(false);
|
|
183
183
|
} catch (error) {
|
|
184
184
|
console.error("❌ Error capturing photo:", error);
|
|
185
185
|
setLoadingBeforeCapture(false);
|
|
186
186
|
Alert.alert(
|
|
187
|
-
"Erreur",
|
|
187
|
+
"Erreur",
|
|
188
188
|
`Impossible de capturer la photo: ${error.message || "Erreur inconnue"}. Veuillez réessayer.`
|
|
189
189
|
);
|
|
190
190
|
}
|
|
191
191
|
}
|
|
192
192
|
};
|
|
193
|
-
|
|
193
|
+
|
|
194
194
|
|
|
195
195
|
return (
|
|
196
196
|
<SafeAreaView style={styles.outerContainer}>
|
|
197
|
-
<View
|
|
197
|
+
<View
|
|
198
198
|
style={styles.cameraWrapper}
|
|
199
199
|
ref={cameraWrapperRef}
|
|
200
200
|
onLayout={(e) => {
|
|
@@ -349,4 +349,4 @@ const styles = StyleSheet.create({
|
|
|
349
349
|
color: GLOW_WHITE,
|
|
350
350
|
fontWeight: '600',
|
|
351
351
|
},
|
|
352
|
-
});
|
|
352
|
+
});
|
package/src/ImageCropper.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import styles from './ImageCropperStyles';
|
|
2
2
|
import React, { useState, useRef, useEffect } from 'react';
|
|
3
|
-
import { Modal, View, Image, Dimensions, TouchableOpacity, Animated, Text, Platform, SafeAreaView, PixelRatio, StyleSheet } from 'react-native';
|
|
3
|
+
import { Modal, View, Image, Dimensions, TouchableOpacity, Animated, Text, Platform, SafeAreaView, PixelRatio, StyleSheet, ActivityIndicator } from 'react-native';
|
|
4
4
|
import Svg, { Path, Circle } from 'react-native-svg';
|
|
5
5
|
import CustomCamera from './CustomCamera';
|
|
6
6
|
import * as ImageManipulator from 'expo-image-manipulator';
|
|
@@ -8,7 +8,8 @@ import { Ionicons } from '@expo/vector-icons';
|
|
|
8
8
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
9
9
|
import { applyMaskToImage, MaskView } from './ImageMaskProcessor';
|
|
10
10
|
|
|
11
|
-
const
|
|
11
|
+
const PRIMARY_GREEN = '#198754';
|
|
12
|
+
const ImageCropper = ({ onConfirm, openCameraFirst, initialImage, addheight, rotationLabel }) => {
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
const [image, setImage] = useState(null);
|
|
@@ -132,6 +133,7 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
132
133
|
const [isLoading, setIsLoading] = useState(false);
|
|
133
134
|
const [showFullScreenCapture, setShowFullScreenCapture] = useState(false);
|
|
134
135
|
const [isRotating, setIsRotating] = useState(false);
|
|
136
|
+
const rotationInProgressRef = useRef(false); // block duplicate taps immediately
|
|
135
137
|
const lastValidPosition = useRef(null);
|
|
136
138
|
const insets = useSafeAreaInsets();
|
|
137
139
|
|
|
@@ -139,7 +141,7 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
139
141
|
|
|
140
142
|
// No view-shot / captureRef / bitmap masking on device.
|
|
141
143
|
const enableMask = false;
|
|
142
|
-
const enableRotation =
|
|
144
|
+
const enableRotation = true; // rotation resets crop box so it is re-initialized on rotated image.
|
|
143
145
|
|
|
144
146
|
|
|
145
147
|
|
|
@@ -1027,32 +1029,34 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
1027
1029
|
|
|
1028
1030
|
// ✅ REFACTORISATION : Stocker l'angle de rotation au lieu de modifier l'image immédiatement
|
|
1029
1031
|
// La rotation sera appliquée uniquement lors du crop final pour éviter les interpolations multiples
|
|
1030
|
-
|
|
1031
|
-
if (!image
|
|
1032
|
+
const rotatePreviewImage = async (degrees) => {
|
|
1033
|
+
if (!image) return;
|
|
1034
|
+
if (rotationInProgressRef.current) return; // block duplicate taps immediately (no re-render delay)
|
|
1035
|
+
rotationInProgressRef.current = true;
|
|
1032
1036
|
setIsRotating(true);
|
|
1033
|
-
|
|
1037
|
+
|
|
1034
1038
|
try {
|
|
1035
|
-
// ✅ CORRECTION : appliquer la rotation de façon incrémentale sur le fichier (pas cumulée sur un fichier déjà roté).
|
|
1036
|
-
// L'ancienne version rotait l'image déjà rotée par l'angle total, ce qui donnait des rotations incorrectes (90 + 180 => 270).
|
|
1037
1039
|
rotationAngle.current = (rotationAngle.current + degrees) % 360;
|
|
1038
1040
|
|
|
1041
|
+
// Use JPEG for preview rotation (faster than PNG for large images; quality 0.92 is fine for preview)
|
|
1039
1042
|
const rotated = await ImageManipulator.manipulateAsync(
|
|
1040
1043
|
image,
|
|
1041
1044
|
[{ rotate: degrees }],
|
|
1042
|
-
{
|
|
1043
|
-
compress:
|
|
1044
|
-
format: ImageManipulator.SaveFormat.
|
|
1045
|
+
{
|
|
1046
|
+
compress: 0.92,
|
|
1047
|
+
format: ImageManipulator.SaveFormat.JPEG,
|
|
1045
1048
|
}
|
|
1046
1049
|
);
|
|
1047
|
-
|
|
1048
|
-
|
|
1050
|
+
|
|
1051
|
+
setPoints([]);
|
|
1052
|
+
hasInitializedCropBox.current = false;
|
|
1049
1053
|
setImage(rotated.uri);
|
|
1050
|
-
|
|
1051
|
-
console.log("Rotation applied (preview increment):", degrees, "degrees; accumulated:", rotationAngle.current);
|
|
1054
|
+
console.log("Rotation applied:", degrees, "degrees; accumulated:", rotationAngle.current);
|
|
1052
1055
|
} catch (error) {
|
|
1053
1056
|
console.error("Error rotating image:", error);
|
|
1054
1057
|
alert("Error rotating image");
|
|
1055
1058
|
} finally {
|
|
1059
|
+
rotationInProgressRef.current = false;
|
|
1056
1060
|
setIsRotating(false);
|
|
1057
1061
|
}
|
|
1058
1062
|
};
|
|
@@ -1214,6 +1218,12 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
1214
1218
|
})()}
|
|
1215
1219
|
</Svg>
|
|
1216
1220
|
</View>
|
|
1221
|
+
{isRotating && (
|
|
1222
|
+
<View style={{ position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'center', alignItems: 'center' }}>
|
|
1223
|
+
<ActivityIndicator size="large" color={PRIMARY_GREEN} />
|
|
1224
|
+
<Text style={{ color: PRIMARY_GREEN, marginTop: 8, fontSize: 14 }}>{rotationLabel ?? 'Rotation...'}</Text>
|
|
1225
|
+
</View>
|
|
1226
|
+
)}
|
|
1217
1227
|
</View>
|
|
1218
1228
|
)}
|
|
1219
1229
|
|
|
@@ -1222,11 +1232,15 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
1222
1232
|
<View style={[styles.buttonContainerBelow, { paddingBottom: Math.max(insets.bottom, 16) }]}>
|
|
1223
1233
|
{Platform.OS === 'android' && (
|
|
1224
1234
|
<TouchableOpacity
|
|
1225
|
-
style={styles.rotationButton}
|
|
1235
|
+
style={[styles.rotationButton, isRotating && { opacity: 0.7 }]}
|
|
1226
1236
|
onPress={() => enableRotation && rotatePreviewImage(90)}
|
|
1227
1237
|
disabled={isRotating}
|
|
1228
1238
|
>
|
|
1229
|
-
|
|
1239
|
+
{isRotating ? (
|
|
1240
|
+
<ActivityIndicator size="small" color="white" />
|
|
1241
|
+
) : (
|
|
1242
|
+
<Ionicons name="sync" size={24} color="white" />
|
|
1243
|
+
)}
|
|
1230
1244
|
</TouchableOpacity>
|
|
1231
1245
|
)}
|
|
1232
1246
|
|