@technotoil/image-video-editor 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/lib/commonjs/assets/frames/film_vintage.png +0 -0
  2. package/lib/commonjs/assets/frames/floral_gold.png +0 -0
  3. package/lib/commonjs/assets/frames/minimal_double.png +0 -0
  4. package/lib/commonjs/assets/frames/polaroid_white.png +0 -0
  5. package/lib/commonjs/assets/frames/watercolor_floral.png +0 -0
  6. package/lib/commonjs/components/VideoEditor.js +167 -0
  7. package/lib/commonjs/components/VideoEditor.js.map +1 -0
  8. package/lib/commonjs/index.js +14 -0
  9. package/lib/commonjs/index.js.map +1 -0
  10. package/lib/commonjs/native/CameraView.js +109 -0
  11. package/lib/commonjs/native/CameraView.js.map +1 -0
  12. package/lib/commonjs/native/FrameGrabber.js +17 -0
  13. package/lib/commonjs/native/FrameGrabber.js.map +1 -0
  14. package/lib/commonjs/native/MediaEditor.js +24 -0
  15. package/lib/commonjs/native/MediaEditor.js.map +1 -0
  16. package/lib/commonjs/native/MediaLibrary.js +45 -0
  17. package/lib/commonjs/native/MediaLibrary.js.map +1 -0
  18. package/lib/commonjs/native/MediaPicker.js +17 -0
  19. package/lib/commonjs/native/MediaPicker.js.map +1 -0
  20. package/lib/commonjs/native/MediaPlayer.js +17 -0
  21. package/lib/commonjs/native/MediaPlayer.js.map +1 -0
  22. package/lib/commonjs/native/VideoPreview.js +17 -0
  23. package/lib/commonjs/native/VideoPreview.js.map +1 -0
  24. package/lib/commonjs/package.json +1 -0
  25. package/lib/commonjs/screens/CropScreen.js +1231 -0
  26. package/lib/commonjs/screens/CropScreen.js.map +1 -0
  27. package/lib/commonjs/screens/EditorScreen.js +5858 -0
  28. package/lib/commonjs/screens/EditorScreen.js.map +1 -0
  29. package/lib/commonjs/screens/ExportScreen.js +294 -0
  30. package/lib/commonjs/screens/ExportScreen.js.map +1 -0
  31. package/lib/commonjs/screens/GalleryScreen.js +510 -0
  32. package/lib/commonjs/screens/GalleryScreen.js.map +1 -0
  33. package/lib/commonjs/screens/PickScreen.js +1261 -0
  34. package/lib/commonjs/screens/PickScreen.js.map +1 -0
  35. package/lib/commonjs/types.js +2 -0
  36. package/lib/commonjs/types.js.map +1 -0
  37. package/lib/module/components/VideoEditor.js +7 -1
  38. package/lib/module/components/VideoEditor.js.map +1 -1
  39. package/lib/module/screens/CropScreen.js +22 -7
  40. package/lib/module/screens/CropScreen.js.map +1 -1
  41. package/lib/module/screens/EditorScreen.js +142 -42
  42. package/lib/module/screens/EditorScreen.js.map +1 -1
  43. package/lib/module/screens/PickScreen.js +76 -16
  44. package/lib/module/screens/PickScreen.js.map +1 -1
  45. package/lib/typescript/src/components/VideoEditor.d.ts +9 -1
  46. package/lib/typescript/src/screens/CropScreen.d.ts +2 -1
  47. package/lib/typescript/src/screens/PickScreen.d.ts +3 -1
  48. package/package.json +14 -8
  49. package/src/components/VideoEditor.tsx +14 -0
  50. package/src/screens/CropScreen.tsx +47 -31
  51. package/src/screens/EditorScreen.tsx +139 -45
  52. package/src/screens/PickScreen.tsx +95 -18
@@ -25,6 +25,7 @@ import type { ImageEditOptions, MediaItem, MusicTrack } from '../types';
25
25
 
26
26
 
27
27
  const SCREEN_WIDTH = Dimensions.get('window').width;
28
+ const SCREEN_HEIGHT = Dimensions.get('window').height;
28
29
  const TIMELINE_WIDTH = SCREEN_WIDTH - 40;
29
30
  const HANDLE_SIZE = 24;
30
31
  const CARD_WIDTH = SCREEN_WIDTH * 0.76;
@@ -472,6 +473,8 @@ export function EditorScreen({
472
473
  const [newText, setNewText] = useState('');
473
474
  const isNewOverlay = React.useRef(false); // track if overlay was just created (not yet saved)
474
475
  const originalOverlayBackup = React.useRef<{ id: string; text: string; x: number; y: number; color: string; fontSize: number } | null>(null);
476
+ const [activeDraggingId, setActiveDraggingId] = useState<string | null>(null);
477
+ const [isOverTrash, setIsOverTrash] = useState(false);
475
478
 
476
479
  // ── Stickers ────────────────────────────────────────────────────────────────
477
480
  const STICKER_LIST = [
@@ -826,10 +829,19 @@ export function EditorScreen({
826
829
  const createTextPan = (id: string) => {
827
830
  let startX = 0;
828
831
  let startY = 0;
832
+ let pressStartTime = 0;
833
+ let hasMoved = false;
834
+ let longPressTimeout: any = null;
835
+
829
836
  return PanResponder.create({
830
837
  onStartShouldSetPanResponder: () => true,
831
838
  onMoveShouldSetPanResponder: () => true,
832
839
  onPanResponderGrant: () => {
840
+ pressStartTime = Date.now();
841
+ hasMoved = false;
842
+ setActiveDraggingId(id);
843
+ setIsOverTrash(false);
844
+
833
845
  setOverlays(prev => {
834
846
  const item = prev.find(o => o.id === id);
835
847
  if (item) {
@@ -838,13 +850,80 @@ export function EditorScreen({
838
850
  }
839
851
  return prev;
840
852
  });
853
+
854
+ // Setup long press timer (600ms)
855
+ if (longPressTimeout) clearTimeout(longPressTimeout);
856
+ longPressTimeout = setTimeout(() => {
857
+ if (!hasMoved) {
858
+ removeTextOverlay(id);
859
+ }
860
+ }, 600);
841
861
  },
842
862
  onPanResponderMove: (_, gesture) => {
863
+ if (Math.abs(gesture.dx) > 4 || Math.abs(gesture.dy) > 4) {
864
+ hasMoved = true;
865
+ if (longPressTimeout) {
866
+ clearTimeout(longPressTimeout);
867
+ longPressTimeout = null;
868
+ }
869
+ }
870
+
871
+ const newX = startX + gesture.dx;
872
+ const newY = startY + gesture.dy;
873
+
874
+ // Trash zone: bottom 140px of screen, center horizontal
875
+ const isNearTrash = gesture.moveY > SCREEN_HEIGHT - 140 && Math.abs(gesture.moveX - (SCREEN_WIDTH / 2)) < 90;
876
+ setIsOverTrash(isNearTrash);
877
+
843
878
  setOverlays(prev => prev.map(o => o.id === id ? {
844
879
  ...o,
845
- x: startX + gesture.dx,
846
- y: startY + gesture.dy
880
+ x: newX,
881
+ y: newY
847
882
  } : o));
883
+ },
884
+ onPanResponderRelease: (_, gesture) => {
885
+ if (longPressTimeout) {
886
+ clearTimeout(longPressTimeout);
887
+ longPressTimeout = null;
888
+ }
889
+
890
+ // Check if released over trash zone using final touch screen coordinates
891
+ const releasedOverTrash = gesture.moveY > SCREEN_HEIGHT - 140 && Math.abs(gesture.moveX - (SCREEN_WIDTH / 2)) < 90;
892
+
893
+ // Reset dragging states
894
+ setActiveDraggingId(null);
895
+ setIsOverTrash(false);
896
+
897
+ if (releasedOverTrash) {
898
+ setTimeout(() => {
899
+ removeTextOverlay(id);
900
+ }, 50);
901
+ return;
902
+ }
903
+
904
+ const pressDuration = Date.now() - pressStartTime;
905
+ if (!hasMoved && pressDuration < 250) {
906
+ pushToHistory();
907
+ setOverlays(prev => {
908
+ const found = prev.find(o => o.id === id);
909
+ if (found) {
910
+ originalOverlayBackup.current = { ...found };
911
+ isNewOverlay.current = false;
912
+ setEditingTextId(id);
913
+ setNewText(found.text);
914
+ setPanel('text');
915
+ }
916
+ return prev;
917
+ });
918
+ }
919
+ },
920
+ onPanResponderTerminate: () => {
921
+ if (longPressTimeout) {
922
+ clearTimeout(longPressTimeout);
923
+ longPressTimeout = null;
924
+ }
925
+ setActiveDraggingId(null);
926
+ setIsOverTrash(false);
848
927
  }
849
928
  });
850
929
  };
@@ -1708,24 +1787,11 @@ export function EditorScreen({
1708
1787
  ]}
1709
1788
  {...responder.panHandlers}
1710
1789
  >
1711
- <Pressable
1712
- onLongPress={() => removeTextOverlay(overlay.id)}
1713
- onPress={() => {
1714
- pushToHistory();
1715
- // Backup original state (text, color, position, size, etc.)
1716
- const found = overlays.find(o => o.id === overlay.id);
1717
- if (found) {
1718
- originalOverlayBackup.current = { ...found };
1719
- }
1720
- isNewOverlay.current = false;
1721
- setEditingTextId(overlay.id);
1722
- setNewText(overlay.text);
1723
- }}
1724
- >
1790
+ <View style={{ padding: 4 }}>
1725
1791
  <Text style={[styles.textOverlay, { color: overlay.color, fontSize: overlay.fontSize }]}>
1726
1792
  {overlay.text}
1727
1793
  </Text>
1728
- </Pressable>
1794
+ </View>
1729
1795
  </View>
1730
1796
  );
1731
1797
  })}
@@ -1964,23 +2030,11 @@ export function EditorScreen({
1964
2030
  ]}
1965
2031
  {...responder.panHandlers}
1966
2032
  >
1967
- <Pressable
1968
- onLongPress={() => removeTextOverlay(overlay.id)}
1969
- onPress={() => {
1970
- pushToHistory();
1971
- const found = overlays.find(o => o.id === overlay.id);
1972
- if (found) {
1973
- originalOverlayBackup.current = { ...found };
1974
- }
1975
- isNewOverlay.current = false;
1976
- setEditingTextId(overlay.id);
1977
- setNewText(overlay.text);
1978
- }}
1979
- >
2033
+ <View style={{ padding: 4 }}>
1980
2034
  <Text style={[styles.textOverlay, { color: overlay.color, fontSize: overlay.fontSize }]}>
1981
2035
  {overlay.text}
1982
2036
  </Text>
1983
- </Pressable>
2037
+ </View>
1984
2038
  </View>
1985
2039
  );
1986
2040
  })}
@@ -2352,23 +2406,11 @@ export function EditorScreen({
2352
2406
  ]}
2353
2407
  {...responder.panHandlers}
2354
2408
  >
2355
- <Pressable
2356
- onLongPress={() => removeTextOverlay(overlay.id)}
2357
- onPress={() => {
2358
- pushToHistory();
2359
- const found = overlays.find(o => o.id === overlay.id);
2360
- if (found) {
2361
- originalOverlayBackup.current = { ...found };
2362
- }
2363
- isNewOverlay.current = false;
2364
- setEditingTextId(overlay.id);
2365
- setNewText(overlay.text);
2366
- }}
2367
- >
2409
+ <View style={{ padding: 4 }}>
2368
2410
  <Text style={[styles.textOverlay, { color: overlay.color, fontSize: overlay.fontSize }]}>
2369
2411
  {overlay.text}
2370
2412
  </Text>
2371
- </Pressable>
2413
+ </View>
2372
2414
  </View>
2373
2415
  );
2374
2416
  })}
@@ -3059,6 +3101,23 @@ export function EditorScreen({
3059
3101
  </View>
3060
3102
  </Modal>
3061
3103
 
3104
+ {/* Global Trash Zone for Drag Delete */}
3105
+ {activeDraggingId !== null && (
3106
+ <View style={styles.globalTrashZone} pointerEvents="none">
3107
+ <View
3108
+ style={[
3109
+ styles.trashZoneContainer,
3110
+ isOverTrash && styles.trashZoneActive,
3111
+ isOverTrash ? { transform: [{ scale: 1.15 }] } : {}
3112
+ ]}
3113
+ >
3114
+ <Ionicons name={isOverTrash ? "trash" : "trash-outline"} size={20} color="#fff" />
3115
+ <Text style={styles.trashZoneText}>
3116
+ {isOverTrash ? "Release to Delete" : "Drag here to delete"}
3117
+ </Text>
3118
+ </View>
3119
+ </View>
3120
+ )}
3062
3121
  </View>
3063
3122
  );
3064
3123
  }
@@ -4163,6 +4222,41 @@ const styles = StyleSheet.create({
4163
4222
  borderWidth: 1,
4164
4223
  borderColor: 'rgba(255, 255, 255, 0.3)',
4165
4224
  },
4225
+ globalTrashZone: {
4226
+ position: 'absolute',
4227
+ bottom: Platform.OS === 'ios' ? 48 : 28,
4228
+ left: 0,
4229
+ right: 0,
4230
+ alignItems: 'center',
4231
+ justifyContent: 'center',
4232
+ zIndex: 9999,
4233
+ },
4234
+ trashZoneContainer: {
4235
+ backgroundColor: 'rgba(0, 0, 0, 0.75)',
4236
+ paddingHorizontal: 20,
4237
+ paddingVertical: 12,
4238
+ borderRadius: 24,
4239
+ flexDirection: 'row',
4240
+ alignItems: 'center',
4241
+ justifyContent: 'center',
4242
+ borderWidth: 1.5,
4243
+ borderColor: 'rgba(255, 255, 255, 0.25)',
4244
+ shadowColor: '#000',
4245
+ shadowOffset: { width: 0, height: 4 },
4246
+ shadowOpacity: 0.4,
4247
+ shadowRadius: 6,
4248
+ elevation: 8,
4249
+ },
4250
+ trashZoneActive: {
4251
+ backgroundColor: '#ef4444',
4252
+ borderColor: '#fca5a5',
4253
+ },
4254
+ trashZoneText: {
4255
+ color: '#ffffff',
4256
+ fontSize: 12,
4257
+ fontWeight: '700',
4258
+ marginLeft: 8,
4259
+ },
4166
4260
  cropIconText: {
4167
4261
  color: '#FFFFFF',
4168
4262
  fontSize: 18,
@@ -50,6 +50,8 @@ export function PickScreen({
50
50
  cameraModes = ['POST', 'STORY', 'REEL'],
51
51
  onCameraModeChange,
52
52
  defaultCameraMode,
53
+ maxSelection = 1,
54
+ aspectRatio = 'free',
53
55
  }: {
54
56
  items: MediaItem[];
55
57
  onPicked: (items: MediaItem[]) => void;
@@ -60,6 +62,8 @@ export function PickScreen({
60
62
  cameraModes?: string[];
61
63
  onCameraModeChange?: (mode: string) => void;
62
64
  defaultCameraMode?: string;
65
+ maxSelection?: number;
66
+ aspectRatio?: '1:1' | '4:3' | '4:5' | '16:9' | '9:16' | 'free';
63
67
  }) {
64
68
  const insets = useSafeAreaInsets();
65
69
  const [tab, setTab] = useState<'GALLERY' | 'PHOTO' | 'VIDEO'>('GALLERY');
@@ -73,7 +77,21 @@ export function PickScreen({
73
77
  const [albums, setAlbums] = useState<Album[]>([]);
74
78
  const [loading, setLoading] = useState(false);
75
79
  const [previewUri, setPreviewUri] = useState<string | null>(null);
76
- const [cropMode, setCropMode] = useState<'1:1' | 'original'>('1:1');
80
+
81
+ // Aspect ratio logic
82
+ const isRatioLocked = aspectRatio !== 'free';
83
+ const ratioMap: Record<string, number> = {
84
+ '1:1': 1,
85
+ '4:3': 4 / 3,
86
+ '4:5': 4 / 5,
87
+ '16:9': 16 / 9,
88
+ '9:16': 9 / 16,
89
+ };
90
+ const previewAspectRatio = isRatioLocked ? ratioMap[aspectRatio] ?? 1 : undefined;
91
+ // When ratio is locked, always use 'cover' (fills the fixed frame)
92
+ const [cropMode, setCropMode] = useState<'1:1' | 'original'>(
93
+ isRatioLocked ? '1:1' : '1:1'
94
+ );
77
95
  const [videoPaused, setVideoPaused] = useState(false);
78
96
  const scaleAnim = useRef(new Animated.Value(1)).current;
79
97
 
@@ -248,7 +266,7 @@ export function PickScreen({
248
266
  setSelectedItems((prev) => {
249
267
  const exists = prev.find((i) => i.id === item.id);
250
268
  if (exists) return prev.filter((i) => i.id !== item.id);
251
- if (prev.length >= 10) return prev;
269
+ if (prev.length >= maxSelection) return prev;
252
270
  return [...prev, item];
253
271
  });
254
272
  }
@@ -479,7 +497,15 @@ export function PickScreen({
479
497
  </View>
480
498
  </Modal>
481
499
 
482
- <Animated.View style={[styles.preview, { transform: [{ scale: scaleAnim }] }]}>
500
+ <Animated.View
501
+ style={[
502
+ styles.preview,
503
+ { transform: [{ scale: scaleAnim }] },
504
+ isRatioLocked && previewAspectRatio
505
+ ? { height: undefined, aspectRatio: previewAspectRatio }
506
+ : {},
507
+ ]}
508
+ >
483
509
  {selectedMedia?.type === 'video' ? (
484
510
  <Pressable style={styles.previewImage} onPress={() => setVideoPaused((v) => !v)}>
485
511
  {playableUri ? (
@@ -488,7 +514,7 @@ export function PickScreen({
488
514
  paused={videoPaused}
489
515
  muted={false}
490
516
  style={styles.previewImage}
491
- resizeMode={cropMode === '1:1' ? 'cover' : 'contain'}
517
+ resizeMode="cover"
492
518
  />
493
519
  ) : (
494
520
  <Image
@@ -507,21 +533,31 @@ export function PickScreen({
507
533
  <Image
508
534
  source={{ uri: previewUri ?? selectedMedia?.uri }}
509
535
  style={[styles.previewImage, cropMode === '1:1' ? styles.squareCrop : styles.originalCrop]}
510
- resizeMode={cropMode === '1:1' ? 'cover' : 'contain'}
536
+ resizeMode={isRatioLocked ? 'cover' : (cropMode === '1:1' ? 'cover' : 'contain')}
511
537
  />
512
538
  )}
513
539
 
514
540
  <View style={styles.previewControls}>
515
- <Pressable
516
- style={styles.cropToggle}
517
- onPress={() => setCropMode((v) => (v === '1:1' ? 'original' : '1:1'))}
518
- >
519
- <Ionicons
520
- name={cropMode === '1:1' ? 'square-outline' : 'expand-outline'}
521
- size={20}
522
- color="#fff"
523
- />
524
- </Pressable>
541
+ {/* Ratio lock badge */}
542
+ {isRatioLocked && (
543
+ <View style={styles.ratioBadge}>
544
+ <Text style={styles.ratioBadgeText}>{aspectRatio}</Text>
545
+ </View>
546
+ )}
547
+
548
+ {/* Crop toggle — hidden when ratio is locked */}
549
+ {!isRatioLocked && (
550
+ <Pressable
551
+ style={styles.cropToggle}
552
+ onPress={() => setCropMode((v) => (v === '1:1' ? 'original' : '1:1'))}
553
+ >
554
+ <Ionicons
555
+ name={cropMode === '1:1' ? 'square-outline' : 'expand-outline'}
556
+ size={20}
557
+ color="#fff"
558
+ />
559
+ </Pressable>
560
+ )}
525
561
  </View>
526
562
  </Animated.View>
527
563
 
@@ -534,9 +570,36 @@ export function PickScreen({
534
570
  <Ionicons name="chevron-down" size={16} color="#fff" style={{ marginLeft: 6 }} />
535
571
  </Pressable>
536
572
 
537
- <Pressable style={[styles.multiSelectBtn, multiSelect && styles.multiSelectBtnActive]} onPress={toggleMultiSelect}>
538
- <Text style={[styles.multiSelectText, multiSelect && styles.multiSelectTextActive]}>{multiSelect ? 'Done' : 'Select'}</Text>
539
- </Pressable>
573
+ {/* Only show Select/Done button when maxSelection > 1 */}
574
+ {maxSelection > 1 && (
575
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
576
+ {multiSelect && (
577
+ <Text style={{ color: '#3b82f6', fontSize: 12, fontWeight: '700' }}>
578
+ {selectedItems.length}/{maxSelection}
579
+ </Text>
580
+ )}
581
+ <Pressable
582
+ style={[
583
+ styles.multiSelectBtn,
584
+ multiSelect && styles.multiSelectBtnActive,
585
+ multiSelect && selectedItems.length === 0 && { opacity: 0.35 },
586
+ ]}
587
+ onPress={() => {
588
+ if (multiSelect) {
589
+ handleNext();
590
+ } else {
591
+ toggleMultiSelect();
592
+ }
593
+ }}
594
+ disabled={multiSelect && selectedItems.length === 0}
595
+ >
596
+ <Text style={[
597
+ styles.multiSelectText,
598
+ multiSelect && styles.multiSelectTextActive,
599
+ ]}>{multiSelect ? 'Done' : 'Select'}</Text>
600
+ </Pressable>
601
+ </View>
602
+ )}
540
603
  </View>
541
604
 
542
605
  <FlatList
@@ -794,6 +857,20 @@ const styles = StyleSheet.create({
794
857
  justifyContent: 'center',
795
858
  },
796
859
  cropIcon: { color: '#fff', fontSize: 16 },
860
+ ratioBadge: {
861
+ backgroundColor: 'rgba(0,0,0,0.65)',
862
+ paddingHorizontal: 10,
863
+ paddingVertical: 5,
864
+ borderRadius: 12,
865
+ borderWidth: 1,
866
+ borderColor: 'rgba(255,255,255,0.3)',
867
+ },
868
+ ratioBadgeText: {
869
+ color: '#fff',
870
+ fontSize: 12,
871
+ fontWeight: '700',
872
+ letterSpacing: 0.5,
873
+ },
797
874
  albumRow: {
798
875
  flexDirection: 'row',
799
876
  justifyContent: 'space-between',