@technotoil/image-video-editor 0.1.0

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 (99) hide show
  1. package/ImageVideoEditor.podspec +21 -0
  2. package/README.md +136 -0
  3. package/android/build.gradle +76 -0
  4. package/android/gradle.properties +5 -0
  5. package/android/src/main/AndroidManifest.xml +13 -0
  6. package/android/src/main/java/com/technotoil/image_videoeditor/FrameGrabberModule.kt +67 -0
  7. package/android/src/main/java/com/technotoil/image_videoeditor/MediaEditorModule.kt +548 -0
  8. package/android/src/main/java/com/technotoil/image_videoeditor/MediaFileUtils.kt +29 -0
  9. package/android/src/main/java/com/technotoil/image_videoeditor/MediaLibraryModule.kt +305 -0
  10. package/android/src/main/java/com/technotoil/image_videoeditor/MediaPackage.kt +26 -0
  11. package/android/src/main/java/com/technotoil/image_videoeditor/MediaPickerModule.kt +111 -0
  12. package/android/src/main/java/com/technotoil/image_videoeditor/MediaPlayerModule.kt +34 -0
  13. package/android/src/main/java/com/technotoil/image_videoeditor/RNCameraViewManager.kt +761 -0
  14. package/android/src/main/java/com/technotoil/image_videoeditor/RNVideoPreviewManager.kt +317 -0
  15. package/ios/PrivacyInfo.xcprivacy +38 -0
  16. package/ios/RNCameraViewManager.m +420 -0
  17. package/ios/RNFrameGrabber.m +61 -0
  18. package/ios/RNMediaEditor.m +905 -0
  19. package/ios/RNMediaLibrary.m +389 -0
  20. package/ios/RNMediaPicker.m +144 -0
  21. package/ios/RNMediaPlayer.m +73 -0
  22. package/ios/RNVideoPreviewManager.m +263 -0
  23. package/ios/frames/film_vintage.png +0 -0
  24. package/ios/frames/floral_gold.png +0 -0
  25. package/ios/frames/minimal_double.png +0 -0
  26. package/ios/frames/polaroid_white.png +0 -0
  27. package/ios/frames/watercolor_floral.png +0 -0
  28. package/lib/module/assets/frames/film_vintage.png +0 -0
  29. package/lib/module/assets/frames/floral_gold.png +0 -0
  30. package/lib/module/assets/frames/minimal_double.png +0 -0
  31. package/lib/module/assets/frames/polaroid_white.png +0 -0
  32. package/lib/module/assets/frames/watercolor_floral.png +0 -0
  33. package/lib/module/components/VideoEditor.js +156 -0
  34. package/lib/module/components/VideoEditor.js.map +1 -0
  35. package/lib/module/index.js +4 -0
  36. package/lib/module/index.js.map +1 -0
  37. package/lib/module/native/CameraView.js +104 -0
  38. package/lib/module/native/CameraView.js.map +1 -0
  39. package/lib/module/native/FrameGrabber.js +13 -0
  40. package/lib/module/native/FrameGrabber.js.map +1 -0
  41. package/lib/module/native/MediaEditor.js +19 -0
  42. package/lib/module/native/MediaEditor.js.map +1 -0
  43. package/lib/module/native/MediaLibrary.js +37 -0
  44. package/lib/module/native/MediaLibrary.js.map +1 -0
  45. package/lib/module/native/MediaPicker.js +13 -0
  46. package/lib/module/native/MediaPicker.js.map +1 -0
  47. package/lib/module/native/MediaPlayer.js +13 -0
  48. package/lib/module/native/MediaPlayer.js.map +1 -0
  49. package/lib/module/native/VideoPreview.js +12 -0
  50. package/lib/module/native/VideoPreview.js.map +1 -0
  51. package/lib/module/package.json +1 -0
  52. package/lib/module/screens/CropScreen.js +1211 -0
  53. package/lib/module/screens/CropScreen.js.map +1 -0
  54. package/lib/module/screens/EditorScreen.js +5752 -0
  55. package/lib/module/screens/EditorScreen.js.map +1 -0
  56. package/lib/module/screens/ExportScreen.js +289 -0
  57. package/lib/module/screens/ExportScreen.js.map +1 -0
  58. package/lib/module/screens/GalleryScreen.js +505 -0
  59. package/lib/module/screens/GalleryScreen.js.map +1 -0
  60. package/lib/module/screens/PickScreen.js +1195 -0
  61. package/lib/module/screens/PickScreen.js.map +1 -0
  62. package/lib/module/types.js +2 -0
  63. package/lib/module/types.js.map +1 -0
  64. package/lib/typescript/src/components/VideoEditor.d.ts +13 -0
  65. package/lib/typescript/src/index.d.ts +2 -0
  66. package/lib/typescript/src/native/CameraView.d.ts +23 -0
  67. package/lib/typescript/src/native/FrameGrabber.d.ts +2 -0
  68. package/lib/typescript/src/native/MediaEditor.d.ts +3 -0
  69. package/lib/typescript/src/native/MediaLibrary.d.ts +16 -0
  70. package/lib/typescript/src/native/MediaPicker.d.ts +2 -0
  71. package/lib/typescript/src/native/MediaPlayer.d.ts +1 -0
  72. package/lib/typescript/src/native/VideoPreview.d.ts +19 -0
  73. package/lib/typescript/src/screens/CropScreen.d.ts +9 -0
  74. package/lib/typescript/src/screens/EditorScreen.d.ts +10 -0
  75. package/lib/typescript/src/screens/ExportScreen.d.ts +9 -0
  76. package/lib/typescript/src/screens/GalleryScreen.d.ts +8 -0
  77. package/lib/typescript/src/screens/PickScreen.d.ts +13 -0
  78. package/lib/typescript/src/types.d.ts +58 -0
  79. package/package.json +101 -0
  80. package/src/assets/frames/film_vintage.png +0 -0
  81. package/src/assets/frames/floral_gold.png +0 -0
  82. package/src/assets/frames/minimal_double.png +0 -0
  83. package/src/assets/frames/polaroid_white.png +0 -0
  84. package/src/assets/frames/watercolor_floral.png +0 -0
  85. package/src/components/VideoEditor.tsx +182 -0
  86. package/src/index.tsx +2 -0
  87. package/src/native/CameraView.tsx +95 -0
  88. package/src/native/FrameGrabber.ts +21 -0
  89. package/src/native/MediaEditor.ts +33 -0
  90. package/src/native/MediaLibrary.ts +69 -0
  91. package/src/native/MediaPicker.ts +17 -0
  92. package/src/native/MediaPlayer.ts +16 -0
  93. package/src/native/VideoPreview.tsx +20 -0
  94. package/src/screens/CropScreen.tsx +968 -0
  95. package/src/screens/EditorScreen.tsx +4517 -0
  96. package/src/screens/ExportScreen.tsx +282 -0
  97. package/src/screens/GalleryScreen.tsx +412 -0
  98. package/src/screens/PickScreen.tsx +1094 -0
  99. package/src/types.ts +58 -0
@@ -0,0 +1,1211 @@
1
+ "use strict";
2
+
3
+ import React, { useState, useRef, useEffect } from 'react';
4
+ import { View, Text, StyleSheet, Dimensions, PanResponder, Pressable, Image, ScrollView, Platform, Alert } from 'react-native';
5
+ import { SafeAreaView } from 'react-native-safe-area-context';
6
+ import { editImage, trimVideo } from "../native/MediaEditor.js";
7
+ import { captureFrame } from "../native/FrameGrabber.js";
8
+ import { VideoPreview } from "../native/VideoPreview.js";
9
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
10
+ const {
11
+ width: SCREEN_WIDTH,
12
+ height: SCREEN_HEIGHT
13
+ } = Dimensions.get('window');
14
+
15
+ // --- Icons (View-based) ---
16
+ const HomeIcon = () => /*#__PURE__*/_jsxs(View, {
17
+ style: iconStyles.homeContainer,
18
+ children: [/*#__PURE__*/_jsx(View, {
19
+ style: iconStyles.homeRoof
20
+ }), /*#__PURE__*/_jsx(View, {
21
+ style: iconStyles.homeBase
22
+ })]
23
+ });
24
+ const UndoIcon = () => /*#__PURE__*/_jsx(View, {
25
+ style: iconStyles.arrowContainer,
26
+ children: /*#__PURE__*/_jsx(Text, {
27
+ style: iconStyles.arrowText,
28
+ children: "\u27F2"
29
+ })
30
+ });
31
+ const RedoIcon = () => /*#__PURE__*/_jsx(View, {
32
+ style: iconStyles.arrowContainer,
33
+ children: /*#__PURE__*/_jsx(Text, {
34
+ style: iconStyles.arrowText,
35
+ children: "\u27F3"
36
+ })
37
+ });
38
+ const EyeIcon = () => /*#__PURE__*/_jsxs(View, {
39
+ style: iconStyles.eyeContainer,
40
+ children: [/*#__PURE__*/_jsx(View, {
41
+ style: iconStyles.eyeOuter
42
+ }), /*#__PURE__*/_jsx(View, {
43
+ style: iconStyles.eyeInner
44
+ })]
45
+ });
46
+ const ShareIcon = () => /*#__PURE__*/_jsxs(View, {
47
+ style: iconStyles.shareContainer,
48
+ children: [/*#__PURE__*/_jsx(View, {
49
+ style: iconStyles.shareArrow
50
+ }), /*#__PURE__*/_jsx(View, {
51
+ style: iconStyles.shareBox
52
+ })]
53
+ });
54
+ const ResetIcon = () => /*#__PURE__*/_jsx(Text, {
55
+ style: iconStyles.resetIconText,
56
+ children: "\u21BA"
57
+ });
58
+ const FlipIcon = () => /*#__PURE__*/_jsxs(View, {
59
+ style: iconStyles.flipContainer,
60
+ children: [/*#__PURE__*/_jsx(View, {
61
+ style: iconStyles.flipHalf
62
+ }), /*#__PURE__*/_jsx(View, {
63
+ style: [iconStyles.flipHalf, iconStyles.flipRight]
64
+ })]
65
+ });
66
+ const RotateIcon = () => /*#__PURE__*/_jsx(Text, {
67
+ style: iconStyles.rotateIconText,
68
+ children: "\u21BB"
69
+ });
70
+ const ChevronDown = () => /*#__PURE__*/_jsx(Text, {
71
+ style: {
72
+ color: '#fff',
73
+ fontSize: 10
74
+ },
75
+ children: "\u25BC"
76
+ });
77
+ const iconStyles = StyleSheet.create({
78
+ homeContainer: {
79
+ width: 24,
80
+ height: 24,
81
+ alignItems: 'center',
82
+ justifyContent: 'center'
83
+ },
84
+ homeRoof: {
85
+ width: 0,
86
+ height: 0,
87
+ borderLeftWidth: 10,
88
+ borderRightWidth: 10,
89
+ borderBottomWidth: 10,
90
+ borderStyle: 'solid',
91
+ backgroundColor: 'transparent',
92
+ borderLeftColor: 'transparent',
93
+ borderRightColor: 'transparent',
94
+ borderBottomColor: '#fff',
95
+ marginBottom: -2
96
+ },
97
+ homeBase: {
98
+ width: 16,
99
+ height: 10,
100
+ backgroundColor: '#fff',
101
+ borderRadius: 1
102
+ },
103
+ arrowContainer: {
104
+ width: 24,
105
+ height: 24,
106
+ alignItems: 'center',
107
+ justifyContent: 'center'
108
+ },
109
+ arrowText: {
110
+ color: '#fff',
111
+ fontSize: 20,
112
+ fontWeight: 'bold'
113
+ },
114
+ eyeContainer: {
115
+ width: 24,
116
+ height: 24,
117
+ alignItems: 'center',
118
+ justifyContent: 'center'
119
+ },
120
+ eyeOuter: {
121
+ width: 18,
122
+ height: 10,
123
+ borderRadius: 5,
124
+ borderWidth: 2,
125
+ borderColor: '#fff'
126
+ },
127
+ eyeInner: {
128
+ position: 'absolute',
129
+ width: 4,
130
+ height: 4,
131
+ borderRadius: 2,
132
+ backgroundColor: '#fff'
133
+ },
134
+ shareContainer: {
135
+ width: 24,
136
+ height: 24,
137
+ alignItems: 'center',
138
+ justifyContent: 'center'
139
+ },
140
+ shareBox: {
141
+ width: 14,
142
+ height: 10,
143
+ borderWidth: 2,
144
+ borderColor: '#fff',
145
+ borderTopWidth: 0,
146
+ borderRadius: 1
147
+ },
148
+ shareArrow: {
149
+ width: 2,
150
+ height: 14,
151
+ backgroundColor: '#fff',
152
+ position: 'absolute',
153
+ top: 2
154
+ },
155
+ resetIconText: {
156
+ color: '#fff',
157
+ fontSize: 18
158
+ },
159
+ flipContainer: {
160
+ width: 24,
161
+ height: 20,
162
+ flexDirection: 'row',
163
+ gap: 2
164
+ },
165
+ flipHalf: {
166
+ flex: 1,
167
+ backgroundColor: '#444',
168
+ borderTopLeftRadius: 4,
169
+ borderBottomLeftRadius: 4
170
+ },
171
+ flipRight: {
172
+ backgroundColor: '#fff',
173
+ borderTopLeftRadius: 0,
174
+ borderBottomLeftRadius: 0,
175
+ borderTopRightRadius: 4,
176
+ borderBottomRightRadius: 4
177
+ },
178
+ rotateIconText: {
179
+ color: '#fff',
180
+ fontSize: 20
181
+ }
182
+ });
183
+ const ratios = [{
184
+ label: 'Free',
185
+ ratio: null
186
+ }, {
187
+ label: 'Square',
188
+ ratio: 1
189
+ }, {
190
+ label: '4:5',
191
+ ratio: 4 / 5
192
+ }, {
193
+ label: '3:4',
194
+ ratio: 3 / 4
195
+ }, {
196
+ label: '9:16',
197
+ ratio: 9 / 16
198
+ }, {
199
+ label: '16:9',
200
+ ratio: 16 / 9
201
+ }, {
202
+ label: '2:3',
203
+ ratio: 2 / 3
204
+ }, {
205
+ label: '3:2',
206
+ ratio: 3 / 2
207
+ }];
208
+ export function CropScreen({
209
+ item,
210
+ onBack,
211
+ onSave
212
+ }) {
213
+ const [straightenAngle, setStraightenAngle] = useState(0);
214
+ const [rotation, setRotation] = useState(0);
215
+ const [selectedRatio, setSelectedRatio] = useState('Free');
216
+ const [isFixedRatio, setIsFixedRatio] = useState(false);
217
+ const [loading, setLoading] = useState(true);
218
+ const [saving, setSaving] = useState(false);
219
+ const [imageSize, setImageSize] = useState({
220
+ width: 0,
221
+ height: 0
222
+ });
223
+ const [mediaLayout, setMediaLayout] = useState({
224
+ x: 0,
225
+ y: 0,
226
+ width: 0,
227
+ height: 0
228
+ });
229
+ const [renderedImageSize, setRenderedImageSize] = useState({
230
+ width: 0,
231
+ height: 0,
232
+ top: 0,
233
+ left: 0
234
+ });
235
+ const renderedImageSizeRef = useRef(renderedImageSize);
236
+
237
+ // Crop State
238
+ const [cropBox, setCropBox] = useState({
239
+ top: 50,
240
+ left: 20,
241
+ width: SCREEN_WIDTH - 40,
242
+ height: 150
243
+ });
244
+ const [resolution, setResolution] = useState({
245
+ w: 0,
246
+ h: 0
247
+ });
248
+
249
+ // REF-BASED BACKUP FOR PANRESPONDERS (Prevents closure traps)
250
+ const cropBoxRef = useRef(cropBox);
251
+ useEffect(() => {
252
+ cropBoxRef.current = cropBox;
253
+ }, [cropBox]);
254
+ useEffect(() => {
255
+ renderedImageSizeRef.current = renderedImageSize;
256
+ }, [renderedImageSize]);
257
+ useEffect(() => {
258
+ if (item.type === 'image') {
259
+ Image.getSize(item.uri, (w, h) => {
260
+ setImageSize({
261
+ width: w,
262
+ height: h
263
+ });
264
+ });
265
+ } else {
266
+ (async () => {
267
+ try {
268
+ const frameUri = await captureFrame(item.uri, {
269
+ timeMs: 0
270
+ });
271
+ Image.getSize(frameUri, (w, h) => {
272
+ setImageSize({
273
+ width: w,
274
+ height: h
275
+ });
276
+ });
277
+ } catch {
278
+ setImageSize({
279
+ width: 1080,
280
+ height: 1920
281
+ });
282
+ }
283
+ })();
284
+ }
285
+ }, [item.uri, item.type]);
286
+
287
+ // Calculate the actual area occupied by the image (contain)
288
+ useEffect(() => {
289
+ if (imageSize.width && mediaLayout.width) {
290
+ const containerRatio = mediaLayout.width / mediaLayout.height;
291
+ const isRotated = rotation % 180 === 90;
292
+ const imgW = isRotated ? imageSize.height : imageSize.width;
293
+ const imgH = isRotated ? imageSize.width : imageSize.height;
294
+ const imageRatio = imgW / imgH;
295
+ let w, h;
296
+ if (imageRatio > containerRatio) {
297
+ w = mediaLayout.width;
298
+ h = w / imageRatio;
299
+ } else {
300
+ h = mediaLayout.height;
301
+ w = h * imageRatio;
302
+ }
303
+ const layoutW = isRotated ? h : w;
304
+ const layoutH = isRotated ? w : h;
305
+ const layoutL = (w - layoutW) / 2;
306
+ const layoutT = (h - layoutH) / 2;
307
+ const rendered = {
308
+ width: w,
309
+ height: h,
310
+ top: (mediaLayout.height - h) / 2,
311
+ left: (mediaLayout.width - w) / 2,
312
+ imgW: layoutW,
313
+ imgH: layoutH,
314
+ imgL: layoutL,
315
+ imgT: layoutT
316
+ };
317
+ setRenderedImageSize(rendered);
318
+ setCropBox(() => {
319
+ const ratioObj = ratios.find(r => r.label === selectedRatio);
320
+ const ratio = ratioObj ? ratioObj.ratio : null;
321
+ if (ratio) {
322
+ let newWidth = rendered.width;
323
+ let newHeight = newWidth / ratio;
324
+ if (newHeight > rendered.height) {
325
+ newHeight = rendered.height;
326
+ newWidth = newHeight * ratio;
327
+ }
328
+ if (newWidth > rendered.width) {
329
+ newWidth = rendered.width;
330
+ newHeight = newWidth / ratio;
331
+ }
332
+ return {
333
+ width: newWidth,
334
+ height: newHeight,
335
+ left: rendered.left + (rendered.width - newWidth) / 2,
336
+ top: rendered.top + (rendered.height - newHeight) / 2
337
+ };
338
+ } else {
339
+ return {
340
+ width: rendered.width,
341
+ height: rendered.height,
342
+ left: rendered.left,
343
+ top: rendered.top
344
+ };
345
+ }
346
+ });
347
+ }
348
+ }, [imageSize, mediaLayout, rotation, selectedRatio]);
349
+ const [flipX, setFlipX] = useState(false);
350
+ const [flipY, setFlipY] = useState(false);
351
+ const handleRatioSelect = (label, ratio) => {
352
+ setSelectedRatio(label);
353
+ };
354
+ const updateResolution = () => {
355
+ if (imageSize.width && renderedImageSize.width) {
356
+ const isRotated = rotation % 180 === 90;
357
+ const pixelWidth = isRotated ? imageSize.height : imageSize.width;
358
+ const scale = pixelWidth / renderedImageSize.width;
359
+ setResolution({
360
+ w: Math.round(cropBox.width * scale),
361
+ h: Math.round(cropBox.height * scale)
362
+ });
363
+ }
364
+ };
365
+ useEffect(() => {
366
+ updateResolution();
367
+ }, [cropBox, renderedImageSize, rotation, imageSize]);
368
+ const initialCrop = useRef({
369
+ top: 0,
370
+ left: 0,
371
+ width: 0,
372
+ height: 0
373
+ }).current;
374
+ const getClampedBox = (left, top, width, height) => {
375
+ const rendered = renderedImageSizeRef.current;
376
+ if (rendered.width === 0) return {
377
+ left,
378
+ top,
379
+ width,
380
+ height
381
+ };
382
+ const minSize = 60;
383
+ const imageRight = rendered.left + rendered.width;
384
+ const imageBottom = rendered.top + rendered.height;
385
+
386
+ // 1. Clamp Position to the VIRTUAL CANVAS (not just original image)
387
+ let l = Math.max(rendered.left, Math.min(left, imageRight - minSize));
388
+ let t = Math.max(rendered.top, Math.min(top, imageBottom - minSize));
389
+
390
+ // 2. Clamp Size to the VIRTUAL CANVAS
391
+ let w = Math.max(minSize, Math.min(width, imageRight - l));
392
+ let h = Math.max(minSize, Math.min(height, imageBottom - t));
393
+ if (l + w > imageRight) l = imageRight - w;
394
+ if (t + h > imageBottom) t = imageBottom - h;
395
+ return {
396
+ left: l,
397
+ top: t,
398
+ width: w,
399
+ height: h
400
+ };
401
+ };
402
+ const updateCrop = newBox => {
403
+ setCropBox(getClampedBox(newBox.left, newBox.top, newBox.width, newBox.height));
404
+ };
405
+ const panMove = useRef(PanResponder.create({
406
+ onStartShouldSetPanResponder: () => true,
407
+ onPanResponderGrant: () => {
408
+ initialCrop.top = cropBoxRef.current.top;
409
+ initialCrop.left = cropBoxRef.current.left;
410
+ initialCrop.width = cropBoxRef.current.width;
411
+ initialCrop.height = cropBoxRef.current.height;
412
+ },
413
+ onPanResponderMove: (_, gesture) => {
414
+ const rendered = renderedImageSizeRef.current;
415
+ let l = initialCrop.left + gesture.dx;
416
+ let t = initialCrop.top + gesture.dy;
417
+ l = Math.max(rendered.left, Math.min(l, rendered.left + rendered.width - initialCrop.width));
418
+ t = Math.max(rendered.top, Math.min(t, rendered.top + rendered.height - initialCrop.height));
419
+ setCropBox(prev => ({
420
+ ...prev,
421
+ left: l,
422
+ top: t
423
+ }));
424
+ },
425
+ onPanResponderRelease: () => updateResolution()
426
+ })).current;
427
+ const createSideResponder = side => PanResponder.create({
428
+ onStartShouldSetPanResponder: () => true,
429
+ onPanResponderGrant: () => {
430
+ initialCrop.top = cropBoxRef.current.top;
431
+ initialCrop.left = cropBoxRef.current.left;
432
+ initialCrop.width = cropBoxRef.current.width;
433
+ initialCrop.height = cropBoxRef.current.height;
434
+ },
435
+ onPanResponderMove: (_, gesture) => {
436
+ let {
437
+ top,
438
+ left,
439
+ width,
440
+ height
441
+ } = initialCrop;
442
+ if (side === 'left') {
443
+ left += gesture.dx;
444
+ width -= gesture.dx;
445
+ } else if (side === 'right') {
446
+ width += gesture.dx;
447
+ }
448
+ updateCrop({
449
+ top,
450
+ left,
451
+ width,
452
+ height
453
+ });
454
+ },
455
+ onPanResponderRelease: () => updateResolution()
456
+ });
457
+ const panLeft = useRef(createSideResponder('left')).current;
458
+ const panRight = useRef(createSideResponder('right')).current;
459
+ const createCornerResponder = corner => PanResponder.create({
460
+ onStartShouldSetPanResponder: () => true,
461
+ onPanResponderGrant: () => {
462
+ initialCrop.top = cropBoxRef.current.top;
463
+ initialCrop.left = cropBoxRef.current.left;
464
+ initialCrop.width = cropBoxRef.current.width;
465
+ initialCrop.height = cropBoxRef.current.height;
466
+ },
467
+ onPanResponderMove: (_, gesture) => {
468
+ let {
469
+ top: t,
470
+ left: l,
471
+ width: w,
472
+ height: h
473
+ } = initialCrop;
474
+ const ratio = ratios.find(r => r.label === selectedRatio)?.ratio;
475
+ if (corner === 'TL') {
476
+ t += gesture.dy;
477
+ l += gesture.dx;
478
+ w -= gesture.dx;
479
+ h -= gesture.dy;
480
+ } else if (corner === 'TR') {
481
+ t += gesture.dy;
482
+ w += gesture.dx;
483
+ h -= gesture.dy;
484
+ } else if (corner === 'BL') {
485
+ l += gesture.dx;
486
+ w -= gesture.dx;
487
+ h += gesture.dy;
488
+ } else if (corner === 'BR') {
489
+ w += gesture.dx;
490
+ h += gesture.dy;
491
+ }
492
+ if (ratio && isFixedRatio) {
493
+ if (corner === 'BR' || corner === 'BL') h = w / ratio;else w = h * ratio;
494
+ }
495
+ updateCrop({
496
+ top: t,
497
+ left: l,
498
+ width: w,
499
+ height: h
500
+ });
501
+ },
502
+ onPanResponderRelease: () => updateResolution()
503
+ });
504
+ const panTL = useRef(createCornerResponder('TL')).current;
505
+ const panTR = useRef(createCornerResponder('TR')).current;
506
+ const panBL = useRef(createCornerResponder('BL')).current;
507
+ const panBR = useRef(createCornerResponder('BR')).current;
508
+ const handleSave = async () => {
509
+ try {
510
+ setSaving(true);
511
+ if (!imageSize.width || !renderedImageSize.width) return;
512
+ const isRotated = rotation % 180 === 90;
513
+ const pixelWidth = isRotated ? imageSize.height : imageSize.width;
514
+ const scale = pixelWidth / renderedImageSize.width;
515
+ const finalCrop = {
516
+ x: Math.round((cropBox.left - renderedImageSize.left) * scale),
517
+ y: Math.round((cropBox.top - renderedImageSize.top) * scale),
518
+ width: Math.round(cropBox.width * scale),
519
+ height: Math.round(cropBox.height * scale)
520
+ };
521
+ const options = {
522
+ rotateDegrees: straightenAngle + rotation,
523
+ flipX,
524
+ flipY,
525
+ crop: finalCrop,
526
+ brightness: 0,
527
+ contrast: 1,
528
+ saturation: 1,
529
+ grayscale: false
530
+ };
531
+ const outUri = item.type === 'image' ? await editImage(item.uri, options) : await trimVideo(item.uri, {
532
+ startMs: 0,
533
+ endMs: item.durationMs || 10000,
534
+ ...options
535
+ });
536
+ onSave(outUri, item.type === 'image' ? outUri : undefined, item.durationMs);
537
+ } catch (err) {
538
+ Alert.alert('Apply failed', err?.message ?? 'Could not process crop.');
539
+ } finally {
540
+ setSaving(false);
541
+ }
542
+ };
543
+ const handleReset = () => {
544
+ setStraightenAngle(0);
545
+ setRotation(0);
546
+ setFlipX(false);
547
+ setFlipY(false);
548
+ setSelectedRatio('Free');
549
+ setIsFixedRatio(false);
550
+ };
551
+ const sliderPan = useRef(PanResponder.create({
552
+ onStartShouldSetPanResponder: () => true,
553
+ onPanResponderMove: (_, gesture) => {
554
+ const delta = gesture.dx * 0.1;
555
+ setStraightenAngle(prev => Math.max(-45, Math.min(45, prev + delta)));
556
+ }
557
+ })).current;
558
+ const renderDial = () => {
559
+ const marks = [];
560
+ for (let i = -30; i <= 30; i++) {
561
+ const isMajor = i % 5 === 0;
562
+ const isCenter = i === 0;
563
+ marks.push(/*#__PURE__*/_jsx(View, {
564
+ style: [styles.dialMark, isMajor && styles.dialMarkLong, isCenter && styles.dialMarkCenter, Math.abs(i - straightenAngle / 2) < 0.5 && styles.dialMarkActive]
565
+ }, i));
566
+ }
567
+ return marks;
568
+ };
569
+ return /*#__PURE__*/_jsx(View, {
570
+ style: styles.container,
571
+ children: /*#__PURE__*/_jsxs(SafeAreaView, {
572
+ style: styles.safeArea,
573
+ edges: ['left', 'right', 'bottom'],
574
+ children: [/*#__PURE__*/_jsxs(View, {
575
+ style: styles.topBar,
576
+ children: [/*#__PURE__*/_jsx(Pressable, {
577
+ onPress: onBack,
578
+ style: styles.topBtn,
579
+ children: /*#__PURE__*/_jsx(Text, {
580
+ style: {
581
+ color: '#fff',
582
+ fontSize: 16
583
+ },
584
+ children: "Cancel"
585
+ })
586
+ }), /*#__PURE__*/_jsx(View, {
587
+ style: {
588
+ width: 60
589
+ }
590
+ }), /*#__PURE__*/_jsx(View, {
591
+ style: {
592
+ width: 60
593
+ }
594
+ })]
595
+ }), /*#__PURE__*/_jsx(View, {
596
+ style: styles.previewContainer,
597
+ children: /*#__PURE__*/_jsxs(View, {
598
+ style: styles.mediaContainer,
599
+ onLayout: e => setMediaLayout(e.nativeEvent.layout),
600
+ children: [renderedImageSize.width > 0 && /*#__PURE__*/_jsx(View, {
601
+ style: {
602
+ position: 'absolute',
603
+ top: renderedImageSize.top,
604
+ left: renderedImageSize.left,
605
+ width: renderedImageSize.width,
606
+ height: renderedImageSize.height,
607
+ backgroundColor: '#000'
608
+ },
609
+ children: item.type === 'video' ? /*#__PURE__*/_jsx(VideoPreview, {
610
+ uri: item.uri,
611
+ paused: false,
612
+ muted: true,
613
+ style: [styles.media, {
614
+ position: 'absolute',
615
+ top: renderedImageSize.imgT,
616
+ left: renderedImageSize.imgL,
617
+ width: renderedImageSize.imgW,
618
+ height: renderedImageSize.imgH,
619
+ transform: [{
620
+ rotate: `${rotation}deg`
621
+ }, {
622
+ rotate: `${straightenAngle}deg`
623
+ }, {
624
+ scaleX: flipX ? -1 : 1
625
+ }, {
626
+ scaleY: flipY ? -1 : 1
627
+ }]
628
+ }],
629
+ resizeMode: "contain"
630
+ }) : /*#__PURE__*/_jsx(Image, {
631
+ source: {
632
+ uri: item.uri
633
+ },
634
+ style: [styles.media, {
635
+ position: 'absolute',
636
+ top: renderedImageSize.imgT,
637
+ left: renderedImageSize.imgL,
638
+ width: renderedImageSize.imgW,
639
+ height: renderedImageSize.imgH,
640
+ transform: [{
641
+ rotate: `${rotation}deg`
642
+ }, {
643
+ rotate: `${straightenAngle}deg`
644
+ }, {
645
+ scaleX: flipX ? -1 : 1
646
+ }, {
647
+ scaleY: flipY ? -1 : 1
648
+ }]
649
+ }],
650
+ resizeMode: "contain"
651
+ })
652
+ }), /*#__PURE__*/_jsx(View, {
653
+ style: [styles.shading, {
654
+ top: renderedImageSize.top,
655
+ left: renderedImageSize.left,
656
+ width: renderedImageSize.width,
657
+ height: cropBox.top - renderedImageSize.top
658
+ }],
659
+ pointerEvents: "none"
660
+ }), /*#__PURE__*/_jsx(View, {
661
+ style: [styles.shading, {
662
+ top: cropBox.top + cropBox.height,
663
+ left: renderedImageSize.left,
664
+ width: renderedImageSize.width,
665
+ height: renderedImageSize.top + renderedImageSize.height - (cropBox.top + cropBox.height)
666
+ }],
667
+ pointerEvents: "none"
668
+ }), /*#__PURE__*/_jsx(View, {
669
+ style: [styles.shading, {
670
+ top: cropBox.top,
671
+ left: renderedImageSize.left,
672
+ width: cropBox.left - renderedImageSize.left,
673
+ height: cropBox.height
674
+ }],
675
+ pointerEvents: "none"
676
+ }), /*#__PURE__*/_jsx(View, {
677
+ style: [styles.shading, {
678
+ top: cropBox.top,
679
+ left: cropBox.left + cropBox.width,
680
+ width: renderedImageSize.left + renderedImageSize.width - (cropBox.left + cropBox.width),
681
+ height: cropBox.height
682
+ }],
683
+ pointerEvents: "none"
684
+ }), /*#__PURE__*/_jsxs(View, {
685
+ style: [styles.cropBox, {
686
+ top: cropBox.top,
687
+ left: cropBox.left,
688
+ width: cropBox.width,
689
+ height: cropBox.height,
690
+ position: 'absolute'
691
+ }],
692
+ children: [/*#__PURE__*/_jsx(View, {
693
+ style: [StyleSheet.absoluteFill, {
694
+ backgroundColor: 'transparent'
695
+ }],
696
+ ...panMove.panHandlers
697
+ }), /*#__PURE__*/_jsx(View, {
698
+ style: [styles.gridV1, {
699
+ pointerEvents: 'none'
700
+ }]
701
+ }), /*#__PURE__*/_jsx(View, {
702
+ style: [styles.gridV2, {
703
+ pointerEvents: 'none'
704
+ }]
705
+ }), /*#__PURE__*/_jsx(View, {
706
+ style: [styles.gridH1, {
707
+ pointerEvents: 'none'
708
+ }]
709
+ }), /*#__PURE__*/_jsx(View, {
710
+ style: [styles.gridH2, {
711
+ pointerEvents: 'none'
712
+ }]
713
+ }), /*#__PURE__*/_jsx(View, {
714
+ style: [styles.corner, styles.cornerTL, {
715
+ zIndex: 1000
716
+ }],
717
+ ...panTL.panHandlers,
718
+ children: /*#__PURE__*/_jsx(View, {
719
+ style: [styles.cornerVisual, {
720
+ borderTopWidth: 4,
721
+ borderLeftWidth: 4
722
+ }]
723
+ })
724
+ }), /*#__PURE__*/_jsx(View, {
725
+ style: [styles.corner, styles.cornerTR, {
726
+ zIndex: 1000
727
+ }],
728
+ ...panTR.panHandlers,
729
+ children: /*#__PURE__*/_jsx(View, {
730
+ style: [styles.cornerVisual, {
731
+ borderTopWidth: 4,
732
+ borderRightWidth: 4
733
+ }]
734
+ })
735
+ }), /*#__PURE__*/_jsx(View, {
736
+ style: [styles.corner, styles.cornerBL, {
737
+ zIndex: 1000
738
+ }],
739
+ ...panBL.panHandlers,
740
+ children: /*#__PURE__*/_jsx(View, {
741
+ style: [styles.cornerVisual, {
742
+ borderBottomWidth: 4,
743
+ borderLeftWidth: 4
744
+ }]
745
+ })
746
+ }), /*#__PURE__*/_jsx(View, {
747
+ style: [styles.corner, styles.cornerBR, {
748
+ zIndex: 1000
749
+ }],
750
+ ...panBR.panHandlers,
751
+ children: /*#__PURE__*/_jsx(View, {
752
+ style: [styles.cornerVisual, {
753
+ borderBottomWidth: 4,
754
+ borderRightWidth: 4
755
+ }]
756
+ })
757
+ }), /*#__PURE__*/_jsx(View, {
758
+ style: [styles.sideHandle, styles.sideHandleLeft, {
759
+ zIndex: 900
760
+ }],
761
+ ...panLeft.panHandlers
762
+ }), /*#__PURE__*/_jsx(View, {
763
+ style: [styles.sideHandle, styles.sideHandleRight, {
764
+ zIndex: 900
765
+ }],
766
+ ...panRight.panHandlers
767
+ })]
768
+ })]
769
+ })
770
+ }), /*#__PURE__*/_jsxs(View, {
771
+ style: styles.bottomPanel,
772
+ children: [/*#__PURE__*/_jsxs(View, {
773
+ style: styles.panelHeader,
774
+ children: [/*#__PURE__*/_jsxs(Pressable, {
775
+ style: styles.resetBtn,
776
+ onPress: handleReset,
777
+ children: [/*#__PURE__*/_jsx(ResetIcon, {}), /*#__PURE__*/_jsx(Text, {
778
+ style: styles.resetText,
779
+ children: "Reset"
780
+ })]
781
+ }), /*#__PURE__*/_jsx(View, {
782
+ style: styles.panelTitleContainer,
783
+ children: /*#__PURE__*/_jsx(Text, {
784
+ style: styles.panelTitle,
785
+ children: "Aspect Ratio"
786
+ })
787
+ }), /*#__PURE__*/_jsx(Pressable, {
788
+ style: [styles.fixedRatioBtn, isFixedRatio && styles.fixedRatioBtnActive],
789
+ onPress: () => setIsFixedRatio(!isFixedRatio),
790
+ children: /*#__PURE__*/_jsx(Text, {
791
+ style: styles.fixedRatioText,
792
+ children: isFixedRatio ? 'Locked' : 'Unlocked'
793
+ })
794
+ })]
795
+ }), /*#__PURE__*/_jsx(View, {
796
+ style: styles.straightenContainer,
797
+ children: /*#__PURE__*/_jsxs(View, {
798
+ style: styles.sliderWrapper,
799
+ children: [/*#__PURE__*/_jsx(Pressable, {
800
+ style: [styles.flipBtn, (flipX || flipY) && {
801
+ backgroundColor: '#333',
802
+ borderRadius: 8
803
+ }],
804
+ onPress: () => setFlipX(!flipX),
805
+ onLongPress: () => setFlipY(!flipY),
806
+ children: /*#__PURE__*/_jsx(FlipIcon, {})
807
+ }), /*#__PURE__*/_jsxs(View, {
808
+ style: styles.dialContainer,
809
+ ...sliderPan.panHandlers,
810
+ children: [/*#__PURE__*/_jsx(View, {
811
+ style: styles.dialCenterLine
812
+ }), /*#__PURE__*/_jsx(View, {
813
+ style: styles.dialMarksWrapper,
814
+ children: renderDial()
815
+ })]
816
+ }), /*#__PURE__*/_jsx(Pressable, {
817
+ style: styles.rotateBtn,
818
+ onPress: () => setRotation(r => (r + 90) % 360),
819
+ children: /*#__PURE__*/_jsx(RotateIcon, {})
820
+ })]
821
+ })
822
+ }), /*#__PURE__*/_jsx(ScrollView, {
823
+ horizontal: true,
824
+ showsHorizontalScrollIndicator: false,
825
+ style: styles.ratioScroll,
826
+ contentContainerStyle: styles.ratioContent,
827
+ children: ratios.map(r => {
828
+ const isSelected = selectedRatio === r.label;
829
+ const boxRatio = r.ratio || 1;
830
+ // Limit box size for icon
831
+ const boxStyle = {
832
+ width: boxRatio > 1 ? 24 : 24 * boxRatio,
833
+ height: boxRatio > 1 ? 24 / boxRatio : 24,
834
+ borderWidth: 1.5,
835
+ borderColor: isSelected ? '#4A8CFF' : '#666',
836
+ borderRadius: 2
837
+ };
838
+ return /*#__PURE__*/_jsxs(Pressable, {
839
+ style: styles.ratioItem,
840
+ onPress: () => handleRatioSelect(r.label, r.ratio),
841
+ children: [/*#__PURE__*/_jsx(View, {
842
+ style: [styles.ratioIconBox, isSelected && styles.ratioIconBoxActive],
843
+ children: /*#__PURE__*/_jsx(View, {
844
+ style: boxStyle
845
+ })
846
+ }), /*#__PURE__*/_jsx(Text, {
847
+ style: [styles.ratioLabel, isSelected && styles.ratioLabelActive],
848
+ children: r.label
849
+ })]
850
+ }, r.label);
851
+ })
852
+ }), /*#__PURE__*/_jsxs(View, {
853
+ style: styles.footerStatus,
854
+ children: [/*#__PURE__*/_jsx(View, {
855
+ style: styles.footerLeft,
856
+ children: /*#__PURE__*/_jsxs(Text, {
857
+ style: styles.resolutionText,
858
+ children: [resolution.w, " \xD7 ", resolution.h, " px"]
859
+ })
860
+ }), /*#__PURE__*/_jsx(Pressable, {
861
+ onPress: handleSave,
862
+ style: styles.primarySaveBtn,
863
+ disabled: saving,
864
+ children: saving ? /*#__PURE__*/_jsx(Text, {
865
+ style: styles.saveText,
866
+ children: "Applying..."
867
+ }) : /*#__PURE__*/_jsx(Text, {
868
+ style: styles.saveText,
869
+ children: "Apply Crop"
870
+ })
871
+ })]
872
+ })]
873
+ })]
874
+ })
875
+ });
876
+ }
877
+ const styles = StyleSheet.create({
878
+ container: {
879
+ flex: 1,
880
+ backgroundColor: '#000'
881
+ },
882
+ safeArea: {
883
+ flex: 1
884
+ },
885
+ quickActions: {
886
+ flexDirection: 'row',
887
+ gap: 12
888
+ },
889
+ quickActionBtn: {
890
+ paddingHorizontal: 12,
891
+ paddingVertical: 6,
892
+ borderRadius: 15,
893
+ backgroundColor: '#1a1a1a',
894
+ borderWidth: 1,
895
+ borderColor: '#333'
896
+ },
897
+ quickActionBtnActive: {
898
+ backgroundColor: '#fff',
899
+ borderColor: '#fff'
900
+ },
901
+ quickActionText: {
902
+ color: '#999',
903
+ fontSize: 10,
904
+ fontWeight: '800'
905
+ },
906
+ quickActionTextActive: {
907
+ color: '#000'
908
+ },
909
+ topBar: {
910
+ flexDirection: 'row',
911
+ alignItems: 'center',
912
+ justifyContent: 'space-between',
913
+ paddingHorizontal: 16,
914
+ height: 56
915
+ },
916
+ topBtn: {
917
+ padding: 8
918
+ },
919
+ saveText: {
920
+ color: '#fff',
921
+ fontSize: 16,
922
+ fontWeight: '700'
923
+ },
924
+ primarySaveBtn: {
925
+ backgroundColor: '#4A8CFF',
926
+ paddingHorizontal: 24,
927
+ paddingVertical: 12,
928
+ borderRadius: 12
929
+ },
930
+ previewContainer: {
931
+ flex: 1,
932
+ backgroundColor: '#000',
933
+ justifyContent: 'center',
934
+ alignItems: 'center'
935
+ },
936
+ mediaContainer: {
937
+ flex: 1,
938
+ // Full flexible space
939
+ width: SCREEN_WIDTH,
940
+ justifyContent: 'center',
941
+ alignItems: 'center'
942
+ },
943
+ media: {
944
+ width: '100%',
945
+ height: '100%'
946
+ },
947
+ cropBox: {
948
+ position: 'absolute',
949
+ borderWidth: 1,
950
+ borderColor: '#4A8CFF',
951
+ backgroundColor: 'transparent'
952
+ },
953
+ gridV1: {
954
+ position: 'absolute',
955
+ left: '33.3%',
956
+ top: 0,
957
+ bottom: 0,
958
+ width: 0.5,
959
+ backgroundColor: 'rgba(255,255,255,0.5)'
960
+ },
961
+ gridV2: {
962
+ position: 'absolute',
963
+ left: '66.6%',
964
+ top: 0,
965
+ bottom: 0,
966
+ width: 0.5,
967
+ backgroundColor: 'rgba(255,255,255,0.5)'
968
+ },
969
+ gridH1: {
970
+ position: 'absolute',
971
+ top: '33.3%',
972
+ left: 0,
973
+ right: 0,
974
+ height: 0.5,
975
+ backgroundColor: 'rgba(255,255,255,0.5)'
976
+ },
977
+ gridH2: {
978
+ position: 'absolute',
979
+ top: '66.6%',
980
+ left: 0,
981
+ right: 0,
982
+ height: 0.5,
983
+ backgroundColor: 'rgba(255,255,255,0.5)'
984
+ },
985
+ corner: {
986
+ position: 'absolute',
987
+ width: 40,
988
+ height: 40,
989
+ justifyContent: 'center',
990
+ alignItems: 'center',
991
+ zIndex: 100
992
+ },
993
+ shading: {
994
+ position: 'absolute',
995
+ backgroundColor: 'rgba(0,0,0,0.6)'
996
+ },
997
+ cornerVisual: {
998
+ width: 20,
999
+ height: 20,
1000
+ borderColor: '#4A8CFF'
1001
+ },
1002
+ cornerTL: {
1003
+ top: -10,
1004
+ left: -10
1005
+ },
1006
+ cornerTR: {
1007
+ top: -10,
1008
+ right: -10
1009
+ },
1010
+ cornerBL: {
1011
+ bottom: -10,
1012
+ left: -10
1013
+ },
1014
+ cornerBR: {
1015
+ bottom: -10,
1016
+ right: -10
1017
+ },
1018
+ sideHandle: {
1019
+ position: 'absolute',
1020
+ width: 10,
1021
+ height: 30,
1022
+ backgroundColor: '#4A8CFF',
1023
+ borderRadius: 2,
1024
+ zIndex: 90
1025
+ },
1026
+ sideHandleLeft: {
1027
+ left: -2,
1028
+ top: '50%',
1029
+ marginTop: -10
1030
+ },
1031
+ sideHandleRight: {
1032
+ right: -2,
1033
+ top: '50%',
1034
+ marginTop: -10
1035
+ },
1036
+ bottomPanel: {
1037
+ backgroundColor: '#111',
1038
+ borderTopLeftRadius: 24,
1039
+ borderTopRightRadius: 24,
1040
+ paddingBottom: Platform.OS === 'ios' ? 20 : 10
1041
+ },
1042
+ panelHeader: {
1043
+ flexDirection: 'row',
1044
+ alignItems: 'center',
1045
+ justifyContent: 'space-between',
1046
+ paddingHorizontal: 20,
1047
+ paddingVertical: 12
1048
+ },
1049
+ resetBtn: {
1050
+ flexDirection: 'row',
1051
+ alignItems: 'center',
1052
+ gap: 6
1053
+ },
1054
+ resetText: {
1055
+ color: '#fff',
1056
+ fontSize: 14,
1057
+ fontWeight: '500'
1058
+ },
1059
+ panelTitleContainer: {
1060
+ flexDirection: 'row',
1061
+ alignItems: 'center',
1062
+ gap: 4
1063
+ },
1064
+ panelTitle: {
1065
+ color: '#fff',
1066
+ fontSize: 18,
1067
+ fontWeight: '600'
1068
+ },
1069
+ straightenContainer: {
1070
+ alignItems: 'center',
1071
+ paddingVertical: 10
1072
+ },
1073
+ straightenText: {
1074
+ color: '#999',
1075
+ fontSize: 12,
1076
+ marginBottom: 10
1077
+ },
1078
+ sliderWrapper: {
1079
+ flexDirection: 'row',
1080
+ alignItems: 'center',
1081
+ width: '100%',
1082
+ paddingHorizontal: 20,
1083
+ gap: 12
1084
+ },
1085
+ flipBtn: {
1086
+ padding: 8
1087
+ },
1088
+ rotateBtn: {
1089
+ padding: 8
1090
+ },
1091
+ dialContainer: {
1092
+ flex: 1,
1093
+ height: 40,
1094
+ backgroundColor: '#222',
1095
+ borderRadius: 12,
1096
+ overflow: 'hidden',
1097
+ justifyContent: 'center',
1098
+ alignItems: 'center'
1099
+ },
1100
+ dialCenterLine: {
1101
+ position: 'absolute',
1102
+ top: 5,
1103
+ bottom: 5,
1104
+ width: 2,
1105
+ backgroundColor: '#fff',
1106
+ zIndex: 10
1107
+ },
1108
+ dialMarksWrapper: {
1109
+ flexDirection: 'row',
1110
+ alignItems: 'center',
1111
+ gap: 8
1112
+ },
1113
+ dialMark: {
1114
+ width: 1,
1115
+ height: 8,
1116
+ backgroundColor: '#444'
1117
+ },
1118
+ dialMarkLong: {
1119
+ height: 14,
1120
+ backgroundColor: '#666'
1121
+ },
1122
+ dialMarkCenter: {
1123
+ backgroundColor: '#fff',
1124
+ height: 18,
1125
+ width: 2
1126
+ },
1127
+ dialMarkActive: {
1128
+ backgroundColor: '#fff'
1129
+ },
1130
+ ratioScroll: {
1131
+ marginTop: 20
1132
+ },
1133
+ ratioContent: {
1134
+ paddingHorizontal: 16,
1135
+ gap: 20
1136
+ },
1137
+ ratioItem: {
1138
+ alignItems: 'center',
1139
+ gap: 6,
1140
+ width: 50
1141
+ },
1142
+ ratioIconBox: {
1143
+ width: 40,
1144
+ height: 40,
1145
+ justifyContent: 'center',
1146
+ alignItems: 'center',
1147
+ borderRadius: 8,
1148
+ backgroundColor: 'transparent'
1149
+ },
1150
+ ratioIconBoxActive: {
1151
+ backgroundColor: '#333'
1152
+ },
1153
+ ratioIcon: {
1154
+ fontSize: 20,
1155
+ color: '#fff'
1156
+ },
1157
+ ratioLabel: {
1158
+ color: '#666',
1159
+ fontSize: 12
1160
+ },
1161
+ ratioLabelActive: {
1162
+ color: '#fff',
1163
+ fontWeight: '600'
1164
+ },
1165
+ footerStatus: {
1166
+ flexDirection: 'row',
1167
+ alignItems: 'center',
1168
+ justifyContent: 'space-between',
1169
+ paddingHorizontal: 20,
1170
+ paddingVertical: 16,
1171
+ borderTopWidth: 0.5,
1172
+ borderTopColor: '#222',
1173
+ marginTop: 10
1174
+ },
1175
+ footerLeft: {
1176
+ flexDirection: 'row',
1177
+ alignItems: 'center',
1178
+ gap: 8
1179
+ },
1180
+ footerIcon: {
1181
+ fontSize: 18
1182
+ },
1183
+ footerText: {
1184
+ color: '#fff',
1185
+ fontSize: 14,
1186
+ fontWeight: '500'
1187
+ },
1188
+ footerCenter: {
1189
+ flex: 1,
1190
+ alignItems: 'center'
1191
+ },
1192
+ resolutionText: {
1193
+ color: '#999',
1194
+ fontSize: 12
1195
+ },
1196
+ fixedRatioBtn: {
1197
+ backgroundColor: '#222',
1198
+ paddingHorizontal: 16,
1199
+ paddingVertical: 8,
1200
+ borderRadius: 20
1201
+ },
1202
+ fixedRatioBtnActive: {
1203
+ backgroundColor: '#333'
1204
+ },
1205
+ fixedRatioText: {
1206
+ color: '#fff',
1207
+ fontSize: 13,
1208
+ fontWeight: '500'
1209
+ }
1210
+ });
1211
+ //# sourceMappingURL=CropScreen.js.map