react-native-expo-cropper 1.2.38 → 1.2.39

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.
@@ -1103,58 +1103,152 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
1103
1103
  />
1104
1104
  ) : (
1105
1105
  <>
1106
- {!showResult && (
1107
- <View style={image ? styles.buttonContainer : styles.centerButtonsContainer}>
1108
-
1109
- {image && Platform.OS === 'android' && (
1110
- <>
1111
- <TouchableOpacity
1112
- style={styles.iconButton}
1113
- onPress={() => enableRotation && rotatePreviewImage(90)}
1114
- disabled={isRotating}
1115
- >
1116
- <Ionicons name="sync" size={24} color="white" />
1117
- </TouchableOpacity>
1118
- </>
1119
- )}
1120
-
1121
- {image && (
1122
- <TouchableOpacity style={styles.button} onPress={handleReset}>
1123
- <Text style={styles.buttonText}>Reset</Text>
1106
+ {image && (
1107
+ <View
1108
+ style={{
1109
+ width: Dimensions.get('window').width,
1110
+ aspectRatio: 9 / 16,
1111
+ borderRadius: 30,
1112
+ overflow: 'hidden',
1113
+ alignItems: 'center',
1114
+ justifyContent: 'center',
1115
+ position: 'relative',
1116
+ backgroundColor: 'black',
1117
+ marginBottom: 0, // ✅ Les boutons sont maintenant en position absolue en bas
1118
+ }}
1119
+ ref={commonWrapperRef}
1120
+ onLayout={onCommonWrapperLayout}
1121
+ >
1122
+ <View
1123
+ ref={viewRef}
1124
+ collapsable={false}
1125
+ style={StyleSheet.absoluteFill}
1126
+ onStartShouldSetResponder={() => true}
1127
+ onMoveShouldSetResponder={(evt, gestureState) => {
1128
+ // ✅ CRITICAL: Always capture movement when a point is selected
1129
+ // This ensures vertical movement is captured correctly
1130
+ if (selectedPointIndex.current !== null) {
1131
+ return true;
1132
+ }
1133
+ // ✅ CRITICAL: Capture ANY movement immediately (even 0px) to prevent ScrollView interception
1134
+ // This is especially important for vertical movement which ScrollView tries to intercept
1135
+ // We return true for ANY movement to ensure we capture it before ScrollView
1136
+ const hasMovement = Math.abs(gestureState.dx) > 0 || Math.abs(gestureState.dy) > 0;
1137
+ if (hasMovement && Math.abs(gestureState.dy) > 5) {
1138
+ console.log("🔄 Vertical movement detected in responder:", {
1139
+ dx: gestureState.dx.toFixed(2),
1140
+ dy: gestureState.dy.toFixed(2),
1141
+ selectedPoint: selectedPointIndex.current
1142
+ });
1143
+ }
1144
+ return true;
1145
+ }}
1146
+ onResponderGrant={(e) => {
1147
+ // ✅ CRITICAL: Grant responder immediately to prevent ScrollView from intercepting
1148
+ // This ensures we capture all movement, especially vertical
1149
+ // Handle tap to select point if needed
1150
+ if (selectedPointIndex.current === null) {
1151
+ handleTap(e);
1152
+ }
1153
+ }}
1154
+ onResponderStart={handleTap}
1155
+ onResponderMove={(e) => {
1156
+ // ✅ CRITICAL: Always handle move events to ensure smooth movement in all directions
1157
+ // This is called for every move event, ensuring vertical movement is captured
1158
+ // handleMove now uses incremental delta calculation which is more reliable
1159
+ handleMove(e);
1160
+ }}
1161
+ onResponderRelease={handleRelease}
1162
+ onResponderTerminationRequest={() => {
1163
+ // ✅ CRITICAL: Never allow termination when dragging a point
1164
+ // This prevents ScrollView from stealing the responder during vertical movement
1165
+ return selectedPointIndex.current === null;
1166
+ }}
1167
+ // ✅ CRITICAL: Prevent parent ScrollView from intercepting touches
1168
+ // Capture responder BEFORE parent ScrollView can intercept
1169
+ onStartShouldSetResponderCapture={() => {
1170
+ // Always capture start events
1171
+ return true;
1172
+ }}
1173
+ onMoveShouldSetResponderCapture={(evt, gestureState) => {
1174
+ // ✅ CRITICAL: Always capture movement events before parent ScrollView
1175
+ // This is essential for vertical movement which ScrollView tries to intercept
1176
+ // Especially important when a point is selected or when there's any movement
1177
+ if (selectedPointIndex.current !== null) {
1178
+ return true;
1179
+ }
1180
+ // ✅ CRITICAL: Capture movement BEFORE ScrollView can intercept
1181
+ // This ensures we get vertical movement even if ScrollView tries to steal it
1182
+ const hasMovement = Math.abs(gestureState.dx) > 0 || Math.abs(gestureState.dy) > 0;
1183
+ return hasMovement;
1184
+ }}
1185
+ >
1186
+ <Image
1187
+ source={{ uri: image }}
1188
+ style={styles.image}
1189
+ resizeMode={cameraFrameData.current?.greenFrame ? 'cover' : 'contain'}
1190
+ onLayout={onImageLayout}
1191
+ />
1192
+ {/* ✅ RÉFÉRENTIEL UNIQUE : SVG overlay utilise les dimensions du wrapper commun */}
1193
+ {/* IMPORTANT: prevent SVG overlay from stealing touch events so dragging works reliably */}
1194
+ <Svg style={styles.overlay} pointerEvents="none">
1195
+ {(() => {
1196
+ // ✅ Use wrapper dimensions for SVG path (wrapper coordinates)
1197
+ const wrapperWidth = commonWrapperLayout.current.width || Dimensions.get('window').width;
1198
+ const wrapperHeight = commonWrapperLayout.current.height || (Dimensions.get('window').width * 16 / 9);
1199
+ return (
1200
+ <>
1201
+ <Path
1202
+ d={`M 0 0 H ${wrapperWidth} V ${wrapperHeight} H 0 Z ${createPath()}`}
1203
+ fill={showResult ? 'white' : 'rgba(0, 0, 0, 0.8)'}
1204
+ fillRule="evenodd"
1205
+ />
1206
+ {!showResult && points.length > 0 && (
1207
+ <Path d={createPath()} fill="transparent" stroke="white" strokeWidth={2} />
1208
+ )}
1209
+ {!showResult && points.map((point, index) => (
1210
+ <Circle key={index} cx={point.x} cy={point.y} r={10} fill="white" />
1211
+ ))}
1212
+ </>
1213
+ );
1214
+ })()}
1215
+ </Svg>
1216
+ </View>
1217
+ </View>
1218
+ )}
1219
+
1220
+ {/* ✅ Buttons positioned BELOW the image, not overlapping */}
1221
+ {!showResult && image && (
1222
+ <View style={[styles.buttonContainerBelow, { paddingBottom: Math.max(insets.bottom, 16) }]}>
1223
+ {Platform.OS === 'android' && (
1224
+ <TouchableOpacity
1225
+ style={styles.rotationButton}
1226
+ onPress={() => enableRotation && rotatePreviewImage(90)}
1227
+ disabled={isRotating}
1228
+ >
1229
+ <Ionicons name="sync" size={24} color="white" />
1124
1230
  </TouchableOpacity>
1125
1231
  )}
1126
- {image && (
1232
+
1233
+ <TouchableOpacity style={styles.button} onPress={handleReset}>
1234
+ <Text style={styles.buttonText}>Reset</Text>
1235
+ </TouchableOpacity>
1236
+
1127
1237
  <TouchableOpacity
1128
1238
  style={styles.button}
1129
- onPress={async () => {
1239
+ onPress={async () => {
1130
1240
  setIsLoading(true);
1131
1241
  try {
1132
1242
  console.log("=== Starting pixel-perfect metadata export (no bitmap crop on mobile) ===");
1133
1243
 
1134
- // ✅ REFACTORISATION : Utiliser les dimensions stockées (plus efficace)
1135
1244
  const actualImageWidth = originalImageDimensions.current.width;
1136
1245
  const actualImageHeight = originalImageDimensions.current.height;
1137
1246
 
1138
- // Vérifier que les dimensions sont valides
1139
1247
  if (actualImageWidth === 0 || actualImageHeight === 0) {
1140
1248
  throw new Error("Original image dimensions not available. Please wait for image to load.");
1141
1249
  }
1142
1250
 
1143
- console.log("Original image dimensions:", {
1144
- width: actualImageWidth,
1145
- height: actualImageHeight
1146
- });
1147
-
1148
- // ✅ CORRECTION : Utiliser le rectangle de contenu réel (contain)
1149
- // ✅ CRITICAL: Recalculate displayedContentRect if not available
1150
1251
  const layout = displayedImageLayout.current;
1151
-
1152
- console.log("🔍 Checking displayedContentRect before crop:", {
1153
- displayedContentRect: displayedContentRect.current,
1154
- displayedImageLayout: layout,
1155
- originalImageDimensions: originalImageDimensions.current
1156
- });
1157
-
1158
1252
  if (layout.width > 0 && layout.height > 0) {
1159
1253
  updateDisplayedContentRect(layout.width, layout.height);
1160
1254
  }
@@ -1163,12 +1257,8 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
1163
1257
  let displayedWidth = contentRect.width;
1164
1258
  let displayedHeight = contentRect.height;
1165
1259
 
1166
- // Vérifier que les dimensions d'affichage sont valides
1167
1260
  if (displayedWidth === 0 || displayedHeight === 0) {
1168
- // ✅ FALLBACK: Try to use displayedImageLayout if contentRect is not available
1169
1261
  if (layout.width > 0 && layout.height > 0) {
1170
- console.warn("⚠️ displayedContentRect not available, using displayedImageLayout as fallback");
1171
- // Use layout dimensions as fallback (assuming no letterboxing)
1172
1262
  contentRect = {
1173
1263
  x: layout.x,
1174
1264
  y: layout.y,
@@ -1177,22 +1267,12 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
1177
1267
  };
1178
1268
  displayedWidth = contentRect.width;
1179
1269
  displayedHeight = contentRect.height;
1180
- // Update the ref for consistency
1181
1270
  displayedContentRect.current = contentRect;
1182
1271
  } else {
1183
- throw new Error("Displayed image dimensions not available. Image may not be laid out yet. Please wait a moment and try again.");
1272
+ throw new Error("Displayed image dimensions not available.");
1184
1273
  }
1185
1274
  }
1186
1275
 
1187
- console.log("✅ Using contentRect for crop:", contentRect);
1188
-
1189
- console.log("Displayed image dimensions:", {
1190
- width: displayedWidth,
1191
- height: displayedHeight
1192
- });
1193
-
1194
- // ✅ CAMERA (cover mode): scale = max, image fills wrapper, has offset
1195
- // ✅ GALLERY (contain mode): scale = min, uniform
1196
1276
  const isCoverMode = !!(cameraFrameData.current && cameraFrameData.current.greenFrame);
1197
1277
  let scale, coverOffsetX = 0, coverOffsetY = 0;
1198
1278
  if (isCoverMode) {
@@ -1201,17 +1281,8 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
1201
1281
  const scaledHeight = actualImageHeight * scale;
1202
1282
  coverOffsetX = (scaledWidth - displayedWidth) / 2;
1203
1283
  coverOffsetY = (scaledHeight - displayedHeight) / 2;
1204
- console.log("Scale factor (COVER mode for camera):", { scale, coverOffsetX, coverOffsetY });
1205
1284
  } else {
1206
- const scaleX = actualImageWidth / displayedWidth;
1207
- const scaleY = actualImageHeight / displayedHeight;
1208
- if (Math.abs(scaleX - scaleY) > 0.01) {
1209
- console.warn("Scale mismatch detected! This may cause incorrect crop coordinates.", {
1210
- scaleX, scaleY, actualImageWidth, actualImageHeight, displayedWidth, displayedHeight
1211
- });
1212
- }
1213
- scale = scaleX;
1214
- console.log("Scale factor (contain, uniform):", { scale, contentRect });
1285
+ scale = actualImageWidth / displayedWidth;
1215
1286
  }
1216
1287
 
1217
1288
  const originalUri = sourceImageUri.current || image;
@@ -1219,15 +1290,9 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
1219
1290
 
1220
1291
  if (points.length > 0) {
1221
1292
  try {
1222
- console.log("Calculating crop boundaries from points...");
1223
- console.log("Points (display coordinates):", points);
1224
- console.log("Content rect (offsets):", contentRect);
1225
-
1226
- // ✅ Conversion display -> image px (contain or cover)
1227
1293
  const imagePoints = points.map(point => {
1228
1294
  let clampedX, clampedY, origX, origY;
1229
1295
  if (isCoverMode) {
1230
- // Cover: display = wrapper, scale = max, image centered with offset
1231
1296
  clampedX = Math.max(0, Math.min(point.x, contentRect.width));
1232
1297
  clampedY = Math.max(0, Math.min(point.y, contentRect.height));
1233
1298
  origX = (clampedX + coverOffsetX) / scale;
@@ -1243,16 +1308,11 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
1243
1308
  return { x: finalX, y: finalY };
1244
1309
  });
1245
1310
 
1246
- console.log("Converted image points (original coordinates):", imagePoints);
1247
-
1248
- // Calculer la bounding box : min X, min Y, max X, max Y
1249
1311
  const minX = Math.min(...imagePoints.map(p => p.x));
1250
1312
  const minY = Math.min(...imagePoints.map(p => p.y));
1251
1313
  const maxX = Math.max(...imagePoints.map(p => p.x));
1252
1314
  const maxY = Math.max(...imagePoints.map(p => p.y));
1253
1315
 
1254
- // ✅ CORRECTION : arrondi "conservateur" (floor origin + ceil end)
1255
- // évite de rogner des pixels et réduit le risque de crop plus petit (perte de détails).
1256
1316
  const cropX = Math.max(0, Math.floor(minX));
1257
1317
  const cropY = Math.max(0, Math.floor(minY));
1258
1318
  const cropEndX = Math.min(actualImageWidth, Math.ceil(maxX));
@@ -1260,25 +1320,8 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
1260
1320
  const cropWidth = Math.max(0, cropEndX - cropX);
1261
1321
  const cropHeight = Math.max(0, cropEndY - cropY);
1262
1322
 
1263
- console.log("Crop parameters (pixel-perfect):", {
1264
- x: cropX,
1265
- y: cropY,
1266
- width: cropWidth,
1267
- height: cropHeight,
1268
- imageWidth: actualImageWidth,
1269
- imageHeight: actualImageHeight,
1270
- boundingBox: {
1271
- minX,
1272
- minY,
1273
- maxX,
1274
- maxY
1275
- }
1276
- });
1277
-
1278
1323
  if (cropWidth > 0 && cropHeight > 0) {
1279
- // 1) bbox in ORIGINAL image pixel coords
1280
1324
  const bbox = { x: cropX, y: cropY, width: cropWidth, height: cropHeight };
1281
- // 2) polygon points relative to bbox (still in ORIGINAL pixel grid)
1282
1325
  const polygon = imagePoints.map(point => ({
1283
1326
  x: point.x - cropX,
1284
1327
  y: point.y - cropY
@@ -1289,21 +1332,14 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
1289
1332
  rotation: 0,
1290
1333
  imageSize: { width: actualImageWidth, height: actualImageHeight },
1291
1334
  };
1292
- console.log("Crop meta ready:", cropMeta);
1293
- } else {
1294
- console.warn("Invalid crop dimensions, cannot export crop meta");
1295
1335
  }
1296
1336
  } catch (cropError) {
1297
1337
  console.error("Error computing crop meta:", cropError);
1298
1338
  }
1299
- } else {
1300
- console.log("No crop points defined, using original image");
1301
1339
  }
1302
1340
 
1303
1341
  const name = `IMAGE XTK${Date.now()}`;
1304
-
1305
1342
  if (onConfirm) {
1306
- console.log("Calling onConfirm with:", originalUri, name, cropMeta);
1307
1343
  onConfirm(originalUri, name, cropMeta);
1308
1344
  }
1309
1345
  } catch (error) {
@@ -1315,131 +1351,16 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
1315
1351
  setShowFullScreenCapture(false);
1316
1352
  }
1317
1353
  }}
1318
- >
1319
- <Text style={styles.buttonText}>Confirm</Text>
1320
- </TouchableOpacity>
1321
- )}
1322
-
1354
+ >
1355
+ <Text style={styles.buttonText}>Confirm</Text>
1356
+ </TouchableOpacity>
1323
1357
  </View>
1324
1358
  )}
1325
1359
 
1326
-
1327
- {image && (
1328
- <View
1329
- style={{
1330
- width: Dimensions.get('window').width,
1331
- aspectRatio: 9 / 16,
1332
- borderRadius: 30,
1333
- overflow: 'hidden',
1334
- alignItems: 'center',
1335
- justifyContent: 'center',
1336
- position: 'relative',
1337
- backgroundColor: 'black',
1338
- }}
1339
- ref={commonWrapperRef}
1340
- onLayout={onCommonWrapperLayout}
1341
- >
1342
- <View
1343
- ref={viewRef}
1344
- collapsable={false}
1345
- style={StyleSheet.absoluteFill}
1346
- onStartShouldSetResponder={() => true}
1347
- onMoveShouldSetResponder={(evt, gestureState) => {
1348
- // ✅ CRITICAL: Always capture movement when a point is selected
1349
- // This ensures vertical movement is captured correctly
1350
- if (selectedPointIndex.current !== null) {
1351
- return true;
1352
- }
1353
- // ✅ CRITICAL: Capture ANY movement immediately (even 0px) to prevent ScrollView interception
1354
- // This is especially important for vertical movement which ScrollView tries to intercept
1355
- // We return true for ANY movement to ensure we capture it before ScrollView
1356
- const hasMovement = Math.abs(gestureState.dx) > 0 || Math.abs(gestureState.dy) > 0;
1357
- if (hasMovement && Math.abs(gestureState.dy) > 5) {
1358
- console.log("🔄 Vertical movement detected in responder:", {
1359
- dx: gestureState.dx.toFixed(2),
1360
- dy: gestureState.dy.toFixed(2),
1361
- selectedPoint: selectedPointIndex.current
1362
- });
1363
- }
1364
- return true;
1365
- }}
1366
- onResponderGrant={(e) => {
1367
- // ✅ CRITICAL: Grant responder immediately to prevent ScrollView from intercepting
1368
- // This ensures we capture all movement, especially vertical
1369
- // Handle tap to select point if needed
1370
- if (selectedPointIndex.current === null) {
1371
- handleTap(e);
1372
- }
1373
- }}
1374
- onResponderStart={handleTap}
1375
- onResponderMove={(e) => {
1376
- // ✅ CRITICAL: Always handle move events to ensure smooth movement in all directions
1377
- // This is called for every move event, ensuring vertical movement is captured
1378
- // handleMove now uses incremental delta calculation which is more reliable
1379
- handleMove(e);
1380
- }}
1381
- onResponderRelease={handleRelease}
1382
- onResponderTerminationRequest={() => {
1383
- // ✅ CRITICAL: Never allow termination when dragging a point
1384
- // This prevents ScrollView from stealing the responder during vertical movement
1385
- return selectedPointIndex.current === null;
1386
- }}
1387
- // ✅ CRITICAL: Prevent parent ScrollView from intercepting touches
1388
- // Capture responder BEFORE parent ScrollView can intercept
1389
- onStartShouldSetResponderCapture={() => {
1390
- // Always capture start events
1391
- return true;
1392
- }}
1393
- onMoveShouldSetResponderCapture={(evt, gestureState) => {
1394
- // ✅ CRITICAL: Always capture movement events before parent ScrollView
1395
- // This is essential for vertical movement which ScrollView tries to intercept
1396
- // Especially important when a point is selected or when there's any movement
1397
- if (selectedPointIndex.current !== null) {
1398
- return true;
1399
- }
1400
- // Also capture if there's any movement to prevent ScrollView from intercepting
1401
- const hasMovement = Math.abs(gestureState.dx) > 0 || Math.abs(gestureState.dy) > 0;
1402
- if (hasMovement && Math.abs(gestureState.dy) > 2) {
1403
- console.log("🔄 Capturing vertical movement before ScrollView:", { dy: gestureState.dy.toFixed(2) });
1404
- }
1405
- return hasMovement;
1406
- }}
1407
- // ✅ CRITICAL: Prevent ScrollView from scrolling by stopping propagation
1408
- onResponderReject={() => {
1409
- console.warn("⚠️ Responder rejected - ScrollView may intercept");
1410
- }}
1411
- >
1412
- <Image
1413
- source={{ uri: image }}
1414
- style={styles.image}
1415
- resizeMode={cameraFrameData.current?.greenFrame ? 'cover' : 'contain'}
1416
- onLayout={onImageLayout}
1417
- />
1418
- {/* ✅ RÉFÉRENTIEL UNIQUE : SVG overlay utilise les dimensions du wrapper commun */}
1419
- {/* IMPORTANT: prevent SVG overlay from stealing touch events so dragging works reliably */}
1420
- <Svg style={styles.overlay} pointerEvents="none">
1421
- {(() => {
1422
- // ✅ Use wrapper dimensions for SVG path (wrapper coordinates)
1423
- const wrapperWidth = commonWrapperLayout.current.width || Dimensions.get('window').width;
1424
- const wrapperHeight = commonWrapperLayout.current.height || (Dimensions.get('window').width * 16 / 9);
1425
- return (
1426
- <>
1427
- <Path
1428
- d={`M 0 0 H ${wrapperWidth} V ${wrapperHeight} H 0 Z ${createPath()}`}
1429
- fill={showResult ? 'white' : 'rgba(0, 0, 0, 0.8)'}
1430
- fillRule="evenodd"
1431
- />
1432
- {!showResult && points.length > 0 && (
1433
- <Path d={createPath()} fill="transparent" stroke="white" strokeWidth={2} />
1434
- )}
1435
- {!showResult && points.map((point, index) => (
1436
- <Circle key={index} cx={point.x} cy={point.y} r={10} fill="white" />
1437
- ))}
1438
- </>
1439
- );
1440
- })()}
1441
- </Svg>
1442
- </View>
1360
+ {/* ✅ Show welcome screen when no image */}
1361
+ {!showResult && !image && (
1362
+ <View style={styles.centerButtonsContainer}>
1363
+ <Text style={styles.welcomeText}>Sélectionnez une image</Text>
1443
1364
  </View>
1444
1365
  )}
1445
1366
  </>
@@ -1452,13 +1373,13 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
1452
1373
  <View
1453
1374
  style={{
1454
1375
  position: 'absolute',
1455
- left: -maskDimensions.width - 100, // Hors écran mais pas trop loin
1376
+ left: -maskDimensions.width - 100,
1456
1377
  top: -maskDimensions.height - 100,
1457
1378
  width: maskDimensions.width,
1458
1379
  height: maskDimensions.height,
1459
- opacity: 1, // Opacité normale pour la capture
1380
+ opacity: 1,
1460
1381
  pointerEvents: 'none',
1461
- zIndex: 9999, // Z-index élevé pour s'assurer qu'elle est au-dessus
1382
+ zIndex: 9999,
1462
1383
  overflow: 'hidden',
1463
1384
  }}
1464
1385
  collapsable={false}
@@ -12,6 +12,7 @@ const styles = StyleSheet.create({
12
12
  container: {
13
13
  flex: 1,
14
14
  alignItems: 'center',
15
+ justifyContent: 'flex-start', // ✅ Start from top, allow content to flow down
15
16
  backgroundColor: DEEP_BLACK,
16
17
  },
17
18
  buttonContainer: {
@@ -27,6 +28,20 @@ const styles = StyleSheet.create({
27
28
  zIndex: 10,
28
29
  gap: 10,
29
30
  },
31
+ buttonContainerBelow: {
32
+ // ✅ Buttons positioned BELOW image, not overlapping, above Android navigation bar
33
+ position: 'absolute',
34
+ bottom: 0,
35
+ left: 0,
36
+ right: 0,
37
+ flexDirection: 'row',
38
+ justifyContent: 'center',
39
+ alignItems: 'center',
40
+ paddingHorizontal: 10,
41
+ paddingTop: 16,
42
+ gap: 8,
43
+ backgroundColor: DEEP_BLACK, // Fond noir pour séparer visuellement
44
+ },
30
45
  iconButton: {
31
46
  backgroundColor: PRIMARY_GREEN,
32
47
  width: 60,
@@ -36,16 +51,24 @@ const styles = StyleSheet.create({
36
51
  justifyContent: 'center',
37
52
  marginRight: 5,
38
53
  },
54
+ rotationButton: {
55
+ // Cercle, même couleur que Reset/Confirm (#549433), même hauteur que les autres boutons
56
+ backgroundColor: '#549433',
57
+ width: 56,
58
+ height: 48, // Même hauteur approximative que les boutons rectangulaires (padding 10 + texte)
59
+ borderRadius: 28,
60
+ alignItems: 'center',
61
+ justifyContent: 'center',
62
+ },
39
63
  button: {
40
64
  flex: 1,
41
- width: "100%",
42
- padding: 10,
65
+ minHeight: 48, // Même hauteur que le bouton de rotation
66
+ paddingVertical: 12,
67
+ paddingHorizontal: 10,
43
68
  alignItems: "center",
44
69
  justifyContent: "center",
45
70
  backgroundColor: "#549433",
46
71
  borderRadius: 5,
47
- marginBottom: 20,
48
- marginRight:5,
49
72
  },
50
73
  buttonText: {
51
74
  color: 'white',