@technotoil/image-video-editor 0.1.0 → 0.1.2

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 (63) hide show
  1. package/README.md +17 -3
  2. package/android/src/main/java/com/technotoil/image_videoeditor/FrameGrabberModule.kt +2 -6
  3. package/android/src/main/java/com/technotoil/image_videoeditor/MediaEditorModule.kt +75 -35
  4. package/android/src/main/java/com/technotoil/image_videoeditor/MediaLibraryModule.kt +51 -35
  5. package/android/src/main/java/com/technotoil/image_videoeditor/RNVideoPreviewManager.kt +98 -117
  6. package/ios/RNMediaEditor.m +38 -7
  7. package/ios/RNMediaLibrary.m +19 -15
  8. package/ios/RNVideoPreviewManager.m +2 -0
  9. package/lib/commonjs/assets/frames/film_vintage.png +0 -0
  10. package/lib/commonjs/assets/frames/floral_gold.png +0 -0
  11. package/lib/commonjs/assets/frames/minimal_double.png +0 -0
  12. package/lib/commonjs/assets/frames/polaroid_white.png +0 -0
  13. package/lib/commonjs/assets/frames/watercolor_floral.png +0 -0
  14. package/lib/commonjs/components/VideoEditor.js +235 -0
  15. package/lib/commonjs/components/VideoEditor.js.map +1 -0
  16. package/lib/commonjs/index.js +14 -0
  17. package/lib/commonjs/index.js.map +1 -0
  18. package/lib/commonjs/native/CameraView.js +109 -0
  19. package/lib/commonjs/native/CameraView.js.map +1 -0
  20. package/lib/commonjs/native/FrameGrabber.js +17 -0
  21. package/lib/commonjs/native/FrameGrabber.js.map +1 -0
  22. package/lib/commonjs/native/MediaEditor.js +24 -0
  23. package/lib/commonjs/native/MediaEditor.js.map +1 -0
  24. package/lib/commonjs/native/MediaLibrary.js +45 -0
  25. package/lib/commonjs/native/MediaLibrary.js.map +1 -0
  26. package/lib/commonjs/native/MediaPicker.js +17 -0
  27. package/lib/commonjs/native/MediaPicker.js.map +1 -0
  28. package/lib/commonjs/native/MediaPlayer.js +17 -0
  29. package/lib/commonjs/native/MediaPlayer.js.map +1 -0
  30. package/lib/commonjs/native/VideoPreview.js +17 -0
  31. package/lib/commonjs/native/VideoPreview.js.map +1 -0
  32. package/lib/commonjs/package.json +1 -0
  33. package/lib/commonjs/screens/CropScreen.js +1233 -0
  34. package/lib/commonjs/screens/CropScreen.js.map +1 -0
  35. package/lib/commonjs/screens/EditorScreen.js +6043 -0
  36. package/lib/commonjs/screens/EditorScreen.js.map +1 -0
  37. package/lib/commonjs/screens/ExportScreen.js +294 -0
  38. package/lib/commonjs/screens/ExportScreen.js.map +1 -0
  39. package/lib/commonjs/screens/GalleryScreen.js +510 -0
  40. package/lib/commonjs/screens/GalleryScreen.js.map +1 -0
  41. package/lib/commonjs/screens/PickScreen.js +1353 -0
  42. package/lib/commonjs/screens/PickScreen.js.map +1 -0
  43. package/lib/commonjs/types.js +2 -0
  44. package/lib/commonjs/types.js.map +1 -0
  45. package/lib/module/components/VideoEditor.js +104 -31
  46. package/lib/module/components/VideoEditor.js.map +1 -1
  47. package/lib/module/screens/CropScreen.js +26 -9
  48. package/lib/module/screens/CropScreen.js.map +1 -1
  49. package/lib/module/screens/EditorScreen.js +371 -86
  50. package/lib/module/screens/EditorScreen.js.map +1 -1
  51. package/lib/module/screens/PickScreen.js +245 -93
  52. package/lib/module/screens/PickScreen.js.map +1 -1
  53. package/lib/typescript/src/components/VideoEditor.d.ts +18 -2
  54. package/lib/typescript/src/screens/CropScreen.d.ts +3 -1
  55. package/lib/typescript/src/screens/EditorScreen.d.ts +2 -1
  56. package/lib/typescript/src/screens/PickScreen.d.ts +6 -1
  57. package/lib/typescript/src/types.d.ts +1 -0
  58. package/package.json +17 -8
  59. package/src/components/VideoEditor.tsx +82 -11
  60. package/src/screens/CropScreen.tsx +54 -33
  61. package/src/screens/EditorScreen.tsx +366 -106
  62. package/src/screens/PickScreen.tsx +231 -76
  63. package/src/types.ts +1 -0
@@ -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;
@@ -193,6 +194,7 @@ export function EditorScreen({
193
194
  onSaved,
194
195
  onOpenCrop,
195
196
  musicList,
197
+ maxVideoDurationMs,
196
198
  }: {
197
199
  items: MediaItem[];
198
200
  initialIndex?: number;
@@ -200,11 +202,16 @@ export function EditorScreen({
200
202
  onSaved: (updatedItems: MediaItem[]) => void;
201
203
  onOpenCrop: (item: MediaItem) => void;
202
204
  musicList?: MusicTrack[];
205
+ maxVideoDurationMs?: number;
203
206
  }) {
204
207
  const [activeIndex, setActiveIndex] = useState(initialIndex);
205
208
  const currentItem = items[activeIndex] || items[0];
206
209
  const item = currentItem; // Aliasing to 'item' for ease of compatibility
207
210
 
211
+ useEffect(() => {
212
+ setActiveIndex(initialIndex);
213
+ }, [initialIndex]);
214
+
208
215
  const [activeFilter, setActiveFilter] = useState('none');
209
216
  const [imageOptions, setImageOptions] = useState<ImageEditOptions>({
210
217
  rotateDegrees: 0,
@@ -215,14 +222,22 @@ export function EditorScreen({
215
222
  saturation: 1,
216
223
  grayscale: false,
217
224
  });
225
+ const [panel, setPanel] = useState<'filter' | 'edit' | 'trim' | 'transform' | 'frame' | 'text' | 'ar' | 'music' | 'sticker' | 'effects' | 'caption' | 'addclip'>(item.type === 'video' ? 'trim' : 'filter');
218
226
  const [trimStart, setTrimStart] = useState(0);
219
- const [trimEnd, setTrimEnd] = useState(item.durationMs || 10000);
227
+ const [trimEnd, setTrimEnd] = useState(() => {
228
+ const end = item.durationMs || 10000;
229
+ return maxVideoDurationMs ? Math.min(end, maxVideoDurationMs) : end;
230
+ });
220
231
 
221
232
  useEffect(() => {
222
233
  setTrimStart(0);
223
- setTrimEnd(item.durationMs || 10000);
234
+ const end = item.durationMs || maxVideoDurationMs || 10000;
235
+ setTrimEnd(maxVideoDurationMs ? Math.min(end, maxVideoDurationMs) : end);
224
236
  setThumbnails([]);
225
- }, [item.id, item.durationMs]);
237
+ if (item.type === 'video' && maxVideoDurationMs && end > maxVideoDurationMs) {
238
+ setPanel('trim');
239
+ }
240
+ }, [item.id, item.durationMs, maxVideoDurationMs]);
226
241
 
227
242
  const [editsHistory, setEditsHistory] = useState<Record<string, any>>({});
228
243
  const editsHistoryRef = useRef<Record<string, any>>({});
@@ -280,7 +295,7 @@ export function EditorScreen({
280
295
  setCropOffset(saved.cropOffset);
281
296
  setZoomScale(saved.zoomScale);
282
297
  setStraightenAngle(saved.straightenAngle);
283
- setIsMuted(saved.isMuted);
298
+ setIsMuted(selectedMusic ? true : saved.isMuted);
284
299
  } else {
285
300
  setActiveFilter('none');
286
301
  setImageOptions({
@@ -293,13 +308,21 @@ export function EditorScreen({
293
308
  grayscale: false,
294
309
  });
295
310
  setTrimStart(0);
296
- setTrimEnd(targetItem.durationMs || 10000);
311
+ const end = targetItem.durationMs || 10000;
312
+ setTrimEnd(maxVideoDurationMs ? Math.min(end, maxVideoDurationMs) : end);
297
313
  setOverlays([]);
298
314
  setCropRatio(null);
299
315
  setCropOffset({ x: 0, y: 0 });
300
316
  setZoomScale(1);
301
317
  setStraightenAngle(0);
302
- setIsMuted(false);
318
+ setIsMuted(selectedMusic ? true : false);
319
+ }
320
+
321
+ // Force trim panel if video is too long
322
+ if (targetItem.type === 'video' && maxVideoDurationMs && targetItem.durationMs && targetItem.durationMs > maxVideoDurationMs) {
323
+ setPanel('trim');
324
+ } else if (!saved) {
325
+ setPanel(targetItem.type === 'video' ? 'trim' : 'filter');
303
326
  }
304
327
  };
305
328
 
@@ -361,13 +384,15 @@ export function EditorScreen({
361
384
  color: o.color,
362
385
  fontSize: o.fontSize * renderScale,
363
386
  })),
387
+ frameUri: edits.imageOptions.frame && FRAME_IMAGES[edits.imageOptions.frame]
388
+ ? Image.resolveAssetSource(FRAME_IMAGES[edits.imageOptions.frame]).uri
389
+ : undefined,
364
390
  };
365
391
  };
366
392
 
367
393
  const [saving, setSaving] = useState(false);
368
394
  const [videoPaused, setVideoPaused] = useState(false);
369
- const [panel, setPanel] = useState<'filter' | 'edit' | 'trim' | 'transform' | 'frame' | 'text' | 'ar' | 'music' | 'sticker' | 'effects' | 'caption' | 'addclip'>(item.type === 'video' ? 'trim' : 'filter');
370
- const resolvedMusicList = musicList || DUMMY_MUSIC_LIST;
395
+ const resolvedMusicList = musicList || [];
371
396
 
372
397
  const [selectedMusic, setSelectedMusic] = useState<MusicTrack | null>(null);
373
398
  const [musicPaused, setMusicPaused] = useState(false);
@@ -472,6 +497,8 @@ export function EditorScreen({
472
497
  const [newText, setNewText] = useState('');
473
498
  const isNewOverlay = React.useRef(false); // track if overlay was just created (not yet saved)
474
499
  const originalOverlayBackup = React.useRef<{ id: string; text: string; x: number; y: number; color: string; fontSize: number } | null>(null);
500
+ const [activeDraggingId, setActiveDraggingId] = useState<string | null>(null);
501
+ const [isOverTrash, setIsOverTrash] = useState(false);
475
502
 
476
503
  // ── Stickers ────────────────────────────────────────────────────────────────
477
504
  const STICKER_LIST = [
@@ -826,10 +853,19 @@ export function EditorScreen({
826
853
  const createTextPan = (id: string) => {
827
854
  let startX = 0;
828
855
  let startY = 0;
856
+ let pressStartTime = 0;
857
+ let hasMoved = false;
858
+ let longPressTimeout: any = null;
859
+
829
860
  return PanResponder.create({
830
861
  onStartShouldSetPanResponder: () => true,
831
862
  onMoveShouldSetPanResponder: () => true,
832
863
  onPanResponderGrant: () => {
864
+ pressStartTime = Date.now();
865
+ hasMoved = false;
866
+ setActiveDraggingId(id);
867
+ setIsOverTrash(false);
868
+
833
869
  setOverlays(prev => {
834
870
  const item = prev.find(o => o.id === id);
835
871
  if (item) {
@@ -838,13 +874,80 @@ export function EditorScreen({
838
874
  }
839
875
  return prev;
840
876
  });
877
+
878
+ // Setup long press timer (600ms)
879
+ if (longPressTimeout) clearTimeout(longPressTimeout);
880
+ longPressTimeout = setTimeout(() => {
881
+ if (!hasMoved) {
882
+ removeTextOverlay(id);
883
+ }
884
+ }, 600);
841
885
  },
842
886
  onPanResponderMove: (_, gesture) => {
887
+ if (Math.abs(gesture.dx) > 4 || Math.abs(gesture.dy) > 4) {
888
+ hasMoved = true;
889
+ if (longPressTimeout) {
890
+ clearTimeout(longPressTimeout);
891
+ longPressTimeout = null;
892
+ }
893
+ }
894
+
895
+ const newX = startX + gesture.dx;
896
+ const newY = startY + gesture.dy;
897
+
898
+ // Trash zone: bottom 140px of screen, center horizontal
899
+ const isNearTrash = gesture.moveY > SCREEN_HEIGHT - 140 && Math.abs(gesture.moveX - (SCREEN_WIDTH / 2)) < 90;
900
+ setIsOverTrash(isNearTrash);
901
+
843
902
  setOverlays(prev => prev.map(o => o.id === id ? {
844
903
  ...o,
845
- x: startX + gesture.dx,
846
- y: startY + gesture.dy
904
+ x: newX,
905
+ y: newY
847
906
  } : o));
907
+ },
908
+ onPanResponderRelease: (_, gesture) => {
909
+ if (longPressTimeout) {
910
+ clearTimeout(longPressTimeout);
911
+ longPressTimeout = null;
912
+ }
913
+
914
+ // Check if released over trash zone using final touch screen coordinates
915
+ const releasedOverTrash = gesture.moveY > SCREEN_HEIGHT - 140 && Math.abs(gesture.moveX - (SCREEN_WIDTH / 2)) < 90;
916
+
917
+ // Reset dragging states
918
+ setActiveDraggingId(null);
919
+ setIsOverTrash(false);
920
+
921
+ if (releasedOverTrash) {
922
+ setTimeout(() => {
923
+ removeTextOverlay(id);
924
+ }, 50);
925
+ return;
926
+ }
927
+
928
+ const pressDuration = Date.now() - pressStartTime;
929
+ if (!hasMoved && pressDuration < 250) {
930
+ pushToHistory();
931
+ setOverlays(prev => {
932
+ const found = prev.find(o => o.id === id);
933
+ if (found) {
934
+ originalOverlayBackup.current = { ...found };
935
+ isNewOverlay.current = false;
936
+ setEditingTextId(id);
937
+ setNewText(found.text);
938
+ setPanel('text');
939
+ }
940
+ return prev;
941
+ });
942
+ }
943
+ },
944
+ onPanResponderTerminate: () => {
945
+ if (longPressTimeout) {
946
+ clearTimeout(longPressTimeout);
947
+ longPressTimeout = null;
948
+ }
949
+ setActiveDraggingId(null);
950
+ setIsOverTrash(false);
848
951
  }
849
952
  });
850
953
  };
@@ -986,12 +1089,19 @@ export function EditorScreen({
986
1089
  color: o.color,
987
1090
  fontSize: o.fontSize * renderScale,
988
1091
  })),
1092
+ frameUri: imageOptions.frame && FRAME_IMAGES[imageOptions.frame]
1093
+ ? Image.resolveAssetSource(FRAME_IMAGES[imageOptions.frame]).uri
1094
+ : undefined,
989
1095
  };
990
1096
  }, [imageOptions, cropOffset, maxPan, dimensions, cropRatio, straightenAngle, overlays]);
991
1097
 
992
1098
  // For visual trim
993
1099
 
994
1100
  const duration = item.durationMs ?? 10_000;
1101
+ const durationRef = useRef(duration);
1102
+ useEffect(() => {
1103
+ durationRef.current = duration;
1104
+ }, [duration]);
995
1105
 
996
1106
  const formatTime = (ms: number) => {
997
1107
  const totalSec = Math.floor(ms / 1000);
@@ -1059,6 +1169,20 @@ export function EditorScreen({
1059
1169
  }
1060
1170
  }, [item.type, item.uri, duration, thumbnails.length]);
1061
1171
 
1172
+ const leftOverlayRef = useRef<View>(null);
1173
+ const rightOverlayRef = useRef<View>(null);
1174
+ const selectionRangeRef = useRef<View>(null);
1175
+ const leftHandleRef = useRef<View>(null);
1176
+ const rightHandleRef = useRef<View>(null);
1177
+
1178
+ const updateNativeRefs = (newStartX: number, newEndX: number) => {
1179
+ leftOverlayRef.current?.setNativeProps({ style: { width: newStartX } });
1180
+ rightOverlayRef.current?.setNativeProps({ style: { left: newEndX } });
1181
+ selectionRangeRef.current?.setNativeProps({ style: { left: newStartX, width: newEndX - newStartX } });
1182
+ leftHandleRef.current?.setNativeProps({ style: { left: newStartX - 16 } });
1183
+ rightHandleRef.current?.setNativeProps({ style: { left: newEndX - 16 } });
1184
+ };
1185
+
1062
1186
  const startPanOffset = useRef(0);
1063
1187
  const startPan = useRef(
1064
1188
  PanResponder.create({
@@ -1073,20 +1197,86 @@ export function EditorScreen({
1073
1197
  setScrollEnabled(false);
1074
1198
  },
1075
1199
  onPanResponderMove: (_, gesture) => {
1076
- const newX = Math.max(0, Math.min(endX.current - 32, startPanOffset.current + gesture.dx));
1200
+ let newX = Math.max(0, Math.min(endX.current - 32, startPanOffset.current + gesture.dx));
1201
+ let newTime = (newX / TIMELINE_WIDTH) * durationRef.current;
1202
+
1203
+ const currentTrimEnd = (endX.current / TIMELINE_WIDTH) * durationRef.current;
1204
+ if (maxVideoDurationMs && currentTrimEnd - newTime > maxVideoDurationMs) {
1205
+ newTime = currentTrimEnd - maxVideoDurationMs;
1206
+ newX = (newTime / durationRef.current) * TIMELINE_WIDTH;
1207
+ }
1208
+
1077
1209
  startX.current = newX;
1078
- const newTime = (newX / TIMELINE_WIDTH) * duration;
1079
- setTrimStart(newTime);
1210
+ updateNativeRefs(newX, endX.current);
1080
1211
  throttledSeek(newTime);
1081
1212
  },
1082
1213
  onPanResponderRelease: () => {
1083
1214
  isDraggingHandle.current = false;
1084
1215
  setScrollEnabled(true);
1216
+ setTrimStart((startX.current / TIMELINE_WIDTH) * durationRef.current);
1217
+ setTrimEnd((endX.current / TIMELINE_WIDTH) * durationRef.current);
1085
1218
  setSeekToMs(-1);
1086
1219
  },
1087
1220
  onPanResponderTerminate: () => {
1088
1221
  isDraggingHandle.current = false;
1089
1222
  setScrollEnabled(true);
1223
+ setTrimStart((startX.current / TIMELINE_WIDTH) * durationRef.current);
1224
+ setTrimEnd((endX.current / TIMELINE_WIDTH) * durationRef.current);
1225
+ setSeekToMs(-1);
1226
+ }
1227
+ })
1228
+ ).current;
1229
+
1230
+ const middlePanOffsetStart = useRef(0);
1231
+ const middlePanOffsetEnd = useRef(0);
1232
+
1233
+ const middlePan = useRef(
1234
+ PanResponder.create({
1235
+ onStartShouldSetPanResponder: () => true,
1236
+ onStartShouldSetPanResponderCapture: () => true,
1237
+ onMoveShouldSetPanResponder: () => true,
1238
+ onMoveShouldSetPanResponderCapture: () => true,
1239
+ onPanResponderGrant: () => {
1240
+ pushToHistory();
1241
+ middlePanOffsetStart.current = startX.current;
1242
+ middlePanOffsetEnd.current = endX.current;
1243
+ isDraggingHandle.current = true;
1244
+ setScrollEnabled(false);
1245
+ },
1246
+ onPanResponderMove: (_, gesture) => {
1247
+ const windowWidth = middlePanOffsetEnd.current - middlePanOffsetStart.current;
1248
+
1249
+ let newStartX = middlePanOffsetStart.current + gesture.dx;
1250
+ let newEndX = middlePanOffsetEnd.current + gesture.dx;
1251
+
1252
+ if (newStartX < 0) {
1253
+ newStartX = 0;
1254
+ newEndX = windowWidth;
1255
+ }
1256
+ if (newEndX > TIMELINE_WIDTH) {
1257
+ newEndX = TIMELINE_WIDTH;
1258
+ newStartX = TIMELINE_WIDTH - windowWidth;
1259
+ }
1260
+
1261
+ startX.current = newStartX;
1262
+ endX.current = newEndX;
1263
+
1264
+ const newStartTime = (newStartX / TIMELINE_WIDTH) * durationRef.current;
1265
+ updateNativeRefs(newStartX, newEndX);
1266
+ throttledSeek(newStartTime);
1267
+ },
1268
+ onPanResponderRelease: () => {
1269
+ isDraggingHandle.current = false;
1270
+ setScrollEnabled(true);
1271
+ setTrimStart((startX.current / TIMELINE_WIDTH) * durationRef.current);
1272
+ setTrimEnd((endX.current / TIMELINE_WIDTH) * durationRef.current);
1273
+ setSeekToMs(-1);
1274
+ },
1275
+ onPanResponderTerminate: () => {
1276
+ isDraggingHandle.current = false;
1277
+ setScrollEnabled(true);
1278
+ setTrimStart((startX.current / TIMELINE_WIDTH) * durationRef.current);
1279
+ setTrimEnd((endX.current / TIMELINE_WIDTH) * durationRef.current);
1090
1280
  setSeekToMs(-1);
1091
1281
  }
1092
1282
  })
@@ -1106,20 +1296,31 @@ export function EditorScreen({
1106
1296
  setScrollEnabled(false);
1107
1297
  },
1108
1298
  onPanResponderMove: (_, gesture) => {
1109
- const newX = Math.min(TIMELINE_WIDTH, Math.max(startX.current + 32, endPanOffset.current + gesture.dx));
1299
+ let newX = Math.min(TIMELINE_WIDTH, Math.max(startX.current + 32, endPanOffset.current + gesture.dx));
1300
+ let newTime = (newX / TIMELINE_WIDTH) * durationRef.current;
1301
+
1302
+ const currentTrimStart = (startX.current / TIMELINE_WIDTH) * durationRef.current;
1303
+ if (maxVideoDurationMs && newTime - currentTrimStart > maxVideoDurationMs) {
1304
+ newTime = currentTrimStart + maxVideoDurationMs;
1305
+ newX = (newTime / durationRef.current) * TIMELINE_WIDTH;
1306
+ }
1307
+
1110
1308
  endX.current = newX;
1111
- const newTime = (newX / TIMELINE_WIDTH) * duration;
1112
- setTrimEnd(newTime);
1309
+ updateNativeRefs(startX.current, newX);
1113
1310
  throttledSeek(newTime);
1114
1311
  },
1115
1312
  onPanResponderRelease: () => {
1116
1313
  isDraggingHandle.current = false;
1117
1314
  setScrollEnabled(true);
1315
+ setTrimStart((startX.current / TIMELINE_WIDTH) * durationRef.current);
1316
+ setTrimEnd((endX.current / TIMELINE_WIDTH) * durationRef.current);
1118
1317
  setSeekToMs(-1);
1119
1318
  },
1120
1319
  onPanResponderTerminate: () => {
1121
1320
  isDraggingHandle.current = false;
1122
1321
  setScrollEnabled(true);
1322
+ setTrimStart((startX.current / TIMELINE_WIDTH) * durationRef.current);
1323
+ setTrimEnd((endX.current / TIMELINE_WIDTH) * durationRef.current);
1123
1324
  setSeekToMs(-1);
1124
1325
  }
1125
1326
  })
@@ -1422,11 +1623,15 @@ export function EditorScreen({
1422
1623
  });
1423
1624
  }
1424
1625
  } else {
1626
+ const safeEndMs = Math.min(trimEnd, item.durationMs || 10000);
1627
+ const safeStartMs = Math.min(trimStart, Math.max(0, safeEndMs - 100));
1628
+ const isFullTrim = trimStart === 0 && trimEnd >= (item.durationMs || 10000);
1629
+
1425
1630
  exportUri = await trimVideo(item.uri, {
1426
- startMs: trimStart,
1427
- endMs: trimEnd,
1631
+ startMs: safeStartMs,
1632
+ endMs: safeEndMs,
1428
1633
  mute: isMuted,
1429
- musicUri: selectedMusic?.url || undefined,
1634
+ ...(selectedMusic?.url ? { musicUri: selectedMusic.url } : {}),
1430
1635
  ...activeOptions,
1431
1636
  });
1432
1637
  }
@@ -1445,6 +1650,7 @@ export function EditorScreen({
1445
1650
  saveEditsForIndex(activeIndex);
1446
1651
 
1447
1652
  const updatedItems = [...items];
1653
+ let cumulativeMusicOffsetMs = 0;
1448
1654
 
1449
1655
  for (let i = 0; i < items.length; i++) {
1450
1656
  const targetItem = items[i];
@@ -1474,6 +1680,7 @@ export function EditorScreen({
1474
1680
  outUri = await trimVideo(outUri, {
1475
1681
  isImage: true,
1476
1682
  musicUri: selectedMusic.url,
1683
+ musicOffsetMs: cumulativeMusicOffsetMs,
1477
1684
  rotateDegrees: 0,
1478
1685
  flipX: false,
1479
1686
  flipY: false,
@@ -1488,12 +1695,18 @@ export function EditorScreen({
1488
1695
  uri: outUri,
1489
1696
  thumbnailUri: outUri,
1490
1697
  };
1698
+ cumulativeMusicOffsetMs += 10000; // Images are 10s by default
1491
1699
  } else {
1700
+ const originalDuration = targetItem.durationMs || maxVideoDurationMs || 10000;
1701
+ const safeEndMs = Math.min(edits.trimEnd, originalDuration);
1702
+ const safeStartMs = Math.min(edits.trimStart, Math.max(0, safeEndMs - 100));
1703
+ const isFullTrim = edits.trimStart === 0 && edits.trimEnd >= originalDuration;
1704
+
1492
1705
  const outUri = await trimVideo(targetItem.uri, {
1493
- startMs: edits.trimStart,
1494
- endMs: edits.trimEnd,
1706
+ startMs: safeStartMs,
1707
+ endMs: safeEndMs,
1495
1708
  mute: edits.isMuted,
1496
- musicUri: selectedMusic?.url || undefined,
1709
+ ...(selectedMusic?.url ? { musicUri: selectedMusic.url, musicOffsetMs: cumulativeMusicOffsetMs } : {}),
1497
1710
  ...opts,
1498
1711
  });
1499
1712
 
@@ -1511,6 +1724,7 @@ export function EditorScreen({
1511
1724
  thumbnailUri: newThumb ? newThumb : targetItem.thumbnailUri,
1512
1725
  durationMs: newDuration,
1513
1726
  };
1727
+ cumulativeMusicOffsetMs += newDuration;
1514
1728
  }
1515
1729
  } else {
1516
1730
  if (selectedMusic) {
@@ -1519,6 +1733,7 @@ export function EditorScreen({
1519
1733
  outUri = await trimVideo(targetItem.uri, {
1520
1734
  isImage: true,
1521
1735
  musicUri: selectedMusic.url,
1736
+ musicOffsetMs: cumulativeMusicOffsetMs,
1522
1737
  rotateDegrees: 0,
1523
1738
  flipX: false,
1524
1739
  flipY: false,
@@ -1527,12 +1742,14 @@ export function EditorScreen({
1527
1742
  saturation: 1,
1528
1743
  grayscale: false,
1529
1744
  });
1745
+ cumulativeMusicOffsetMs += 10000;
1530
1746
  } else {
1747
+ const safeEndMs = targetItem.durationMs || 10000;
1531
1748
  outUri = await trimVideo(targetItem.uri, {
1532
1749
  startMs: 0,
1533
- endMs: targetItem.durationMs || 10000,
1750
+ endMs: safeEndMs,
1534
1751
  mute: isMuted,
1535
- musicUri: selectedMusic.url,
1752
+ ...(selectedMusic?.url ? { musicUri: selectedMusic.url, musicOffsetMs: cumulativeMusicOffsetMs } : {}),
1536
1753
  rotateDegrees: 0,
1537
1754
  flipX: false,
1538
1755
  flipY: false,
@@ -1541,6 +1758,7 @@ export function EditorScreen({
1541
1758
  saturation: 1,
1542
1759
  grayscale: false,
1543
1760
  });
1761
+ cumulativeMusicOffsetMs += safeEndMs;
1544
1762
  }
1545
1763
  updatedItems[i] = {
1546
1764
  ...targetItem,
@@ -1708,24 +1926,11 @@ export function EditorScreen({
1708
1926
  ]}
1709
1927
  {...responder.panHandlers}
1710
1928
  >
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
- >
1929
+ <View style={{ padding: 4 }}>
1725
1930
  <Text style={[styles.textOverlay, { color: overlay.color, fontSize: overlay.fontSize }]}>
1726
1931
  {overlay.text}
1727
1932
  </Text>
1728
- </Pressable>
1933
+ </View>
1729
1934
  </View>
1730
1935
  );
1731
1936
  })}
@@ -1964,23 +2169,11 @@ export function EditorScreen({
1964
2169
  ]}
1965
2170
  {...responder.panHandlers}
1966
2171
  >
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
- >
2172
+ <View style={{ padding: 4 }}>
1980
2173
  <Text style={[styles.textOverlay, { color: overlay.color, fontSize: overlay.fontSize }]}>
1981
2174
  {overlay.text}
1982
2175
  </Text>
1983
- </Pressable>
2176
+ </View>
1984
2177
  </View>
1985
2178
  );
1986
2179
  })}
@@ -2078,7 +2271,8 @@ export function EditorScreen({
2078
2271
  {thumbnails.map((uri, idx) => (
2079
2272
  <Image key={idx} source={{ uri }} style={styles.filmstripImage} />
2080
2273
  ))}
2081
- <View style={styles.timelineOverlay} />
2274
+ <View style={[styles.timelineOverlay, { left: 0, width: (trimStart / duration) * TIMELINE_WIDTH }]} />
2275
+ <View style={[styles.timelineOverlay, { left: (trimEnd / duration) * TIMELINE_WIDTH, right: 0 }]} />
2082
2276
  <View
2083
2277
  style={[
2084
2278
  styles.selectionRange,
@@ -2092,7 +2286,6 @@ export function EditorScreen({
2092
2286
  styles.customHandleLeft,
2093
2287
  { left: (trimStart / duration) * TIMELINE_WIDTH - 16 }
2094
2288
  ]}
2095
- {...startPan.panHandlers}
2096
2289
  >
2097
2290
  <View style={styles.handleBarLine} />
2098
2291
  </View>
@@ -2100,21 +2293,22 @@ export function EditorScreen({
2100
2293
  style={[
2101
2294
  styles.customHandle,
2102
2295
  styles.customHandleRight,
2103
- { left: (trimEnd / duration) * TIMELINE_WIDTH }
2296
+ { left: (trimEnd / duration) * TIMELINE_WIDTH - 16 }
2104
2297
  ]}
2105
- {...endPan.panHandlers}
2106
2298
  >
2107
2299
  <View style={styles.handleBarLine} />
2108
2300
  </View>
2109
2301
  </View>
2110
2302
 
2111
2303
  {/* Sub-track 1: Add Audio */}
2112
- <Pressable style={styles.subTrackRow} onPress={() => setShowMusicModal(true)}>
2113
- <Text style={styles.subTrackIcon}>+</Text>
2114
- <Text style={styles.subTrackText}>
2115
- {selectedMusic ? `Audio: ${selectedMusic.title}` : 'Add audio'}
2116
- </Text>
2117
- </Pressable>
2304
+ {resolvedMusicList.length > 0 && (
2305
+ <Pressable style={styles.subTrackRow} onPress={() => setShowMusicModal(true)}>
2306
+ <Text style={styles.subTrackIcon}>+</Text>
2307
+ <Text style={styles.subTrackText}>
2308
+ {selectedMusic ? `Audio: ${selectedMusic.title}` : 'Add audio'}
2309
+ </Text>
2310
+ </Pressable>
2311
+ )}
2118
2312
 
2119
2313
  {/* Sub-track 2: Add Text */}
2120
2314
  <Pressable style={styles.subTrackRow} onPress={addTextOverlay}>
@@ -2255,12 +2449,14 @@ export function EditorScreen({
2255
2449
  </View>
2256
2450
  <Text style={styles.toolLabel}>Text</Text>
2257
2451
  </Pressable>
2258
- <Pressable style={[styles.toolButton, showMusicModal && styles.toolButtonActive]} onPress={() => setShowMusicModal(true)}>
2259
- <View style={styles.toolIconContainer}>
2260
- <Ionicons name="musical-notes" size={22} color="#fff" />
2261
- </View>
2262
- <Text style={styles.toolLabel}>Audio</Text>
2263
- </Pressable>
2452
+ {resolvedMusicList.length > 0 && (
2453
+ <Pressable style={[styles.toolButton, showMusicModal && styles.toolButtonActive]} onPress={() => setShowMusicModal(true)}>
2454
+ <View style={styles.toolIconContainer}>
2455
+ <Ionicons name="musical-notes" size={22} color="#fff" />
2456
+ </View>
2457
+ <Text style={styles.toolLabel}>Audio</Text>
2458
+ </Pressable>
2459
+ )}
2264
2460
  <Pressable style={[styles.toolButton, panel === 'transform' && styles.toolButtonActive]} onPress={() => setPanel(panel === 'transform' ? 'trim' : 'transform')}>
2265
2461
  <View style={styles.toolIconContainer}>
2266
2462
  <Ionicons name="crop" size={22} color="#fff" />
@@ -2352,23 +2548,11 @@ export function EditorScreen({
2352
2548
  ]}
2353
2549
  {...responder.panHandlers}
2354
2550
  >
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
- >
2551
+ <View style={{ padding: 4 }}>
2368
2552
  <Text style={[styles.textOverlay, { color: overlay.color, fontSize: overlay.fontSize }]}>
2369
2553
  {overlay.text}
2370
2554
  </Text>
2371
- </Pressable>
2555
+ </View>
2372
2556
  </View>
2373
2557
  );
2374
2558
  })}
@@ -2378,12 +2562,14 @@ export function EditorScreen({
2378
2562
  <View style={styles.fullscreenBottomContainer}>
2379
2563
  {/* Bottom Toolbar */}
2380
2564
  <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={[styles.toolButtonsRow, { flexGrow: 1 }]}>
2381
- <Pressable style={[styles.toolButton, showMusicModal && styles.toolButtonActive]} onPress={() => setShowMusicModal(true)}>
2382
- <View style={styles.toolIconContainer}>
2383
- <Ionicons name="musical-notes" size={22} color="#fff" />
2384
- </View>
2385
- <Text style={styles.toolLabel}>Audio</Text>
2386
- </Pressable>
2565
+ {resolvedMusicList.length > 0 && (
2566
+ <Pressable style={[styles.toolButton, showMusicModal && styles.toolButtonActive]} onPress={() => setShowMusicModal(true)}>
2567
+ <View style={styles.toolIconContainer}>
2568
+ <Ionicons name="musical-notes" size={22} color="#fff" />
2569
+ </View>
2570
+ <Text style={styles.toolLabel}>Audio</Text>
2571
+ </Pressable>
2572
+ )}
2387
2573
  <Pressable style={styles.toolButton} onPress={addTextOverlay}>
2388
2574
  <View style={styles.toolIconContainer}>
2389
2575
  <Ionicons name="text" size={22} color="#fff" />
@@ -2469,6 +2655,12 @@ export function EditorScreen({
2469
2655
  activeIndex,
2470
2656
  ]}
2471
2657
  keyExtractor={(it) => it.id}
2658
+ initialScrollIndex={activeIndex}
2659
+ getItemLayout={(_, index) => ({
2660
+ length: SCREEN_WIDTH,
2661
+ offset: SCREEN_WIDTH * index,
2662
+ index,
2663
+ })}
2472
2664
  horizontal
2473
2665
  pagingEnabled={false}
2474
2666
  showsHorizontalScrollIndicator={false}
@@ -2548,7 +2740,7 @@ export function EditorScreen({
2548
2740
  <View style={{ alignItems: 'center', marginBottom: 6 }}>
2549
2741
  <Text style={{ color: '#fff', fontSize: 13, fontWeight: '700' }}>
2550
2742
  {(() => {
2551
- const selMs = trimEnd - trimStart;
2743
+ const selMs = ((endX.current - startX.current) / TIMELINE_WIDTH) * duration;
2552
2744
  const totalSec = Math.floor(selMs / 1000);
2553
2745
  return totalSec >= 60
2554
2746
  ? `${Math.floor(totalSec / 60)}:${(totalSec % 60).toString().padStart(2, '0')} selected`
@@ -2558,32 +2750,43 @@ export function EditorScreen({
2558
2750
  </View>
2559
2751
  <View style={[styles.trimTimelineBox, { overflow: 'visible' }]}>
2560
2752
  <View style={styles.filmstrip}>
2561
- {thumbnails.map((uri, idx) => (
2562
- <Image key={idx} source={{ uri }} style={styles.filmstripImage} />
2563
- ))}
2564
- <View style={styles.timelineOverlay} />
2753
+ {thumbnails.length === 0 ? (
2754
+ <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
2755
+ <ActivityIndicator color="#ffffff" size="small" />
2756
+ </View>
2757
+ ) : (
2758
+ thumbnails.map((uri, idx) => (
2759
+ <Image key={idx} source={{ uri }} style={styles.filmstripImage} />
2760
+ ))
2761
+ )}
2762
+ <View ref={leftOverlayRef} style={[styles.timelineOverlay, { left: 0, width: startX.current }]} />
2763
+ <View ref={rightOverlayRef} style={[styles.timelineOverlay, { left: endX.current, right: 0 }]} />
2565
2764
  <View
2765
+ ref={selectionRangeRef}
2566
2766
  style={[
2567
2767
  styles.selectionRange,
2568
- { left: (trimStart / duration) * TIMELINE_WIDTH, width: ((trimEnd - trimStart) / duration) * TIMELINE_WIDTH }
2768
+ { left: startX.current, width: endX.current - startX.current }
2569
2769
  ]}
2770
+ {...middlePan.panHandlers}
2570
2771
  />
2571
2772
  </View>
2572
2773
  <View
2774
+ ref={leftHandleRef}
2573
2775
  style={[
2574
2776
  styles.customHandle,
2575
2777
  styles.customHandleLeft,
2576
- { left: (trimStart / duration) * TIMELINE_WIDTH - 16 }
2778
+ { left: startX.current - 16 }
2577
2779
  ]}
2578
2780
  {...startPan.panHandlers}
2579
2781
  >
2580
2782
  <View style={styles.handleBarLine} />
2581
2783
  </View>
2582
2784
  <View
2785
+ ref={rightHandleRef}
2583
2786
  style={[
2584
2787
  styles.customHandle,
2585
2788
  styles.customHandleRight,
2586
- { left: (trimEnd / duration) * TIMELINE_WIDTH }
2789
+ { left: endX.current - 16 }
2587
2790
  ]}
2588
2791
  {...endPan.panHandlers}
2589
2792
  >
@@ -2768,12 +2971,14 @@ export function EditorScreen({
2768
2971
  {/* Tools row and bottom navigation controls */}
2769
2972
  <View style={styles.bottomToolBarContainer}>
2770
2973
  <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={[styles.toolButtonsRow, { flexGrow: 1 }]}>
2771
- <Pressable style={[styles.toolButton, showMusicModal && styles.toolButtonActive]} onPress={() => setShowMusicModal(true)}>
2772
- <View style={styles.toolIconContainer}>
2773
- <Ionicons name="musical-notes" size={22} color="#fff" />
2774
- </View>
2775
- <Text style={styles.toolLabel}>Audio</Text>
2776
- </Pressable>
2974
+ {resolvedMusicList.length > 0 && (
2975
+ <Pressable style={[styles.toolButton, showMusicModal && styles.toolButtonActive]} onPress={() => setShowMusicModal(true)}>
2976
+ <View style={styles.toolIconContainer}>
2977
+ <Ionicons name="musical-notes" size={22} color="#fff" />
2978
+ </View>
2979
+ <Text style={styles.toolLabel}>Audio</Text>
2980
+ </Pressable>
2981
+ )}
2777
2982
  <Pressable style={[styles.toolButton, panel === 'text' && styles.toolButtonActive]} onPress={() => setPanel('text')}>
2778
2983
  <View style={styles.toolIconContainer}>
2779
2984
  <Ionicons name="text" size={22} color="#fff" />
@@ -2966,6 +3171,7 @@ export function EditorScreen({
2966
3171
  onPress={() => {
2967
3172
  setSelectedMusic(null);
2968
3173
  setMusicPaused(true);
3174
+ setIsMuted(false);
2969
3175
  }}
2970
3176
  style={styles.musicFooterRemoveBtn}
2971
3177
  >
@@ -3059,6 +3265,23 @@ export function EditorScreen({
3059
3265
  </View>
3060
3266
  </Modal>
3061
3267
 
3268
+ {/* Global Trash Zone for Drag Delete */}
3269
+ {activeDraggingId !== null && (
3270
+ <View style={styles.globalTrashZone} pointerEvents="none">
3271
+ <View
3272
+ style={[
3273
+ styles.trashZoneContainer,
3274
+ isOverTrash && styles.trashZoneActive,
3275
+ isOverTrash ? { transform: [{ scale: 1.15 }] } : {}
3276
+ ]}
3277
+ >
3278
+ <Ionicons name={isOverTrash ? "trash" : "trash-outline"} size={20} color="#fff" />
3279
+ <Text style={styles.trashZoneText}>
3280
+ {isOverTrash ? "Release to Delete" : "Drag here to delete"}
3281
+ </Text>
3282
+ </View>
3283
+ </View>
3284
+ )}
3062
3285
  </View>
3063
3286
  );
3064
3287
  }
@@ -3592,7 +3815,9 @@ const styles = StyleSheet.create({
3592
3815
  },
3593
3816
  filmstripImage: { width: TIMELINE_WIDTH / 10, height: 60 },
3594
3817
  timelineOverlay: {
3595
- ...StyleSheet.absoluteFillObject,
3818
+ position: 'absolute',
3819
+ top: 0,
3820
+ bottom: 0,
3596
3821
  backgroundColor: 'rgba(0,0,0,0.6)',
3597
3822
  },
3598
3823
  selectionRange: {
@@ -3602,7 +3827,7 @@ const styles = StyleSheet.create({
3602
3827
  backgroundColor: 'transparent',
3603
3828
  borderTopWidth: 2,
3604
3829
  borderBottomWidth: 2,
3605
- borderColor: '#fff',
3830
+ borderColor: '#FFD60A',
3606
3831
  },
3607
3832
  handle: {
3608
3833
  position: 'absolute',
@@ -3625,7 +3850,7 @@ const styles = StyleSheet.create({
3625
3850
  top: 0,
3626
3851
  width: 16,
3627
3852
  height: 60,
3628
- backgroundColor: '#FFFFFF',
3853
+ backgroundColor: '#FFD60A',
3629
3854
  justifyContent: 'center',
3630
3855
  alignItems: 'center',
3631
3856
  zIndex: 20,
@@ -4163,6 +4388,41 @@ const styles = StyleSheet.create({
4163
4388
  borderWidth: 1,
4164
4389
  borderColor: 'rgba(255, 255, 255, 0.3)',
4165
4390
  },
4391
+ globalTrashZone: {
4392
+ position: 'absolute',
4393
+ bottom: Platform.OS === 'ios' ? 48 : 28,
4394
+ left: 0,
4395
+ right: 0,
4396
+ alignItems: 'center',
4397
+ justifyContent: 'center',
4398
+ zIndex: 9999,
4399
+ },
4400
+ trashZoneContainer: {
4401
+ backgroundColor: 'rgba(0, 0, 0, 0.75)',
4402
+ paddingHorizontal: 20,
4403
+ paddingVertical: 12,
4404
+ borderRadius: 24,
4405
+ flexDirection: 'row',
4406
+ alignItems: 'center',
4407
+ justifyContent: 'center',
4408
+ borderWidth: 1.5,
4409
+ borderColor: 'rgba(255, 255, 255, 0.25)',
4410
+ shadowColor: '#000',
4411
+ shadowOffset: { width: 0, height: 4 },
4412
+ shadowOpacity: 0.4,
4413
+ shadowRadius: 6,
4414
+ elevation: 8,
4415
+ },
4416
+ trashZoneActive: {
4417
+ backgroundColor: '#ef4444',
4418
+ borderColor: '#fca5a5',
4419
+ },
4420
+ trashZoneText: {
4421
+ color: '#ffffff',
4422
+ fontSize: 12,
4423
+ fontWeight: '700',
4424
+ marginLeft: 8,
4425
+ },
4166
4426
  cropIconText: {
4167
4427
  color: '#FFFFFF',
4168
4428
  fontSize: 18,