@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
@@ -10,6 +10,7 @@ import Video from 'react-native-video';
10
10
  import Ionicons from 'react-native-vector-icons/Ionicons';
11
11
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
12
12
  const SCREEN_WIDTH = Dimensions.get('window').width;
13
+ const SCREEN_HEIGHT = Dimensions.get('window').height;
13
14
  const TIMELINE_WIDTH = SCREEN_WIDTH - 40;
14
15
  const HANDLE_SIZE = 24;
15
16
  const CARD_WIDTH = SCREEN_WIDTH * 0.76;
@@ -167,12 +168,16 @@ export function EditorScreen({
167
168
  onBack,
168
169
  onSaved,
169
170
  onOpenCrop,
170
- musicList
171
+ musicList,
172
+ maxVideoDurationMs
171
173
  }) {
172
174
  const [activeIndex, setActiveIndex] = useState(initialIndex);
173
175
  const currentItem = items[activeIndex] || items[0];
174
176
  const item = currentItem; // Aliasing to 'item' for ease of compatibility
175
177
 
178
+ useEffect(() => {
179
+ setActiveIndex(initialIndex);
180
+ }, [initialIndex]);
176
181
  const [activeFilter, setActiveFilter] = useState('none');
177
182
  const [imageOptions, setImageOptions] = useState({
178
183
  rotateDegrees: 0,
@@ -183,13 +188,21 @@ export function EditorScreen({
183
188
  saturation: 1,
184
189
  grayscale: false
185
190
  });
191
+ const [panel, setPanel] = useState(item.type === 'video' ? 'trim' : 'filter');
186
192
  const [trimStart, setTrimStart] = useState(0);
187
- const [trimEnd, setTrimEnd] = useState(item.durationMs || 10000);
193
+ const [trimEnd, setTrimEnd] = useState(() => {
194
+ const end = item.durationMs || 10000;
195
+ return maxVideoDurationMs ? Math.min(end, maxVideoDurationMs) : end;
196
+ });
188
197
  useEffect(() => {
189
198
  setTrimStart(0);
190
- setTrimEnd(item.durationMs || 10000);
199
+ const end = item.durationMs || maxVideoDurationMs || 10000;
200
+ setTrimEnd(maxVideoDurationMs ? Math.min(end, maxVideoDurationMs) : end);
191
201
  setThumbnails([]);
192
- }, [item.id, item.durationMs]);
202
+ if (item.type === 'video' && maxVideoDurationMs && end > maxVideoDurationMs) {
203
+ setPanel('trim');
204
+ }
205
+ }, [item.id, item.durationMs, maxVideoDurationMs]);
193
206
  const [editsHistory, setEditsHistory] = useState({});
194
207
  const editsHistoryRef = useRef({});
195
208
  const [dimensionsMap, setDimensionsMap] = useState({});
@@ -246,7 +259,7 @@ export function EditorScreen({
246
259
  setCropOffset(saved.cropOffset);
247
260
  setZoomScale(saved.zoomScale);
248
261
  setStraightenAngle(saved.straightenAngle);
249
- setIsMuted(saved.isMuted);
262
+ setIsMuted(selectedMusic ? true : saved.isMuted);
250
263
  } else {
251
264
  setActiveFilter('none');
252
265
  setImageOptions({
@@ -259,7 +272,8 @@ export function EditorScreen({
259
272
  grayscale: false
260
273
  });
261
274
  setTrimStart(0);
262
- setTrimEnd(targetItem.durationMs || 10000);
275
+ const end = targetItem.durationMs || 10000;
276
+ setTrimEnd(maxVideoDurationMs ? Math.min(end, maxVideoDurationMs) : end);
263
277
  setOverlays([]);
264
278
  setCropRatio(null);
265
279
  setCropOffset({
@@ -268,7 +282,14 @@ export function EditorScreen({
268
282
  });
269
283
  setZoomScale(1);
270
284
  setStraightenAngle(0);
271
- setIsMuted(false);
285
+ setIsMuted(selectedMusic ? true : false);
286
+ }
287
+
288
+ // Force trim panel if video is too long
289
+ if (targetItem.type === 'video' && maxVideoDurationMs && targetItem.durationMs && targetItem.durationMs > maxVideoDurationMs) {
290
+ setPanel('trim');
291
+ } else if (!saved) {
292
+ setPanel(targetItem.type === 'video' ? 'trim' : 'filter');
272
293
  }
273
294
  };
274
295
  const handleScrollEnd = e => {
@@ -327,13 +348,13 @@ export function EditorScreen({
327
348
  y: (o.y + 8) * renderScale,
328
349
  color: o.color,
329
350
  fontSize: o.fontSize * renderScale
330
- }))
351
+ })),
352
+ frameUri: edits.imageOptions.frame && FRAME_IMAGES[edits.imageOptions.frame] ? Image.resolveAssetSource(FRAME_IMAGES[edits.imageOptions.frame]).uri : undefined
331
353
  };
332
354
  };
333
355
  const [saving, setSaving] = useState(false);
334
356
  const [videoPaused, setVideoPaused] = useState(false);
335
- const [panel, setPanel] = useState(item.type === 'video' ? 'trim' : 'filter');
336
- const resolvedMusicList = musicList || DUMMY_MUSIC_LIST;
357
+ const resolvedMusicList = musicList || [];
337
358
  const [selectedMusic, setSelectedMusic] = useState(null);
338
359
  const [musicPaused, setMusicPaused] = useState(false);
339
360
  const [showMusicModal, setShowMusicModal] = useState(false);
@@ -428,6 +449,8 @@ export function EditorScreen({
428
449
  const [newText, setNewText] = useState('');
429
450
  const isNewOverlay = React.useRef(false); // track if overlay was just created (not yet saved)
430
451
  const originalOverlayBackup = React.useRef(null);
452
+ const [activeDraggingId, setActiveDraggingId] = useState(null);
453
+ const [isOverTrash, setIsOverTrash] = useState(false);
431
454
 
432
455
  // ── Stickers ────────────────────────────────────────────────────────────────
433
456
  const STICKER_LIST = ['😂', '❤️', '🔥', '✨', '💯', '👑', '🎉', '🌈', '🎶', '💫', '🙌', '😍', '🤩', '😎', '🥳', '🌸', '🦋', '⚡', '🌙', '💎', '🍕', '🎸', '🎬', '📸', '🎯', '🌺', '🦄', '🐉', '🎭', '🪩'];
@@ -894,10 +917,17 @@ export function EditorScreen({
894
917
  const createTextPan = id => {
895
918
  let startX = 0;
896
919
  let startY = 0;
920
+ let pressStartTime = 0;
921
+ let hasMoved = false;
922
+ let longPressTimeout = null;
897
923
  return PanResponder.create({
898
924
  onStartShouldSetPanResponder: () => true,
899
925
  onMoveShouldSetPanResponder: () => true,
900
926
  onPanResponderGrant: () => {
927
+ pressStartTime = Date.now();
928
+ hasMoved = false;
929
+ setActiveDraggingId(id);
930
+ setIsOverTrash(false);
901
931
  setOverlays(prev => {
902
932
  const item = prev.find(o => o.id === id);
903
933
  if (item) {
@@ -906,13 +936,78 @@ export function EditorScreen({
906
936
  }
907
937
  return prev;
908
938
  });
939
+
940
+ // Setup long press timer (600ms)
941
+ if (longPressTimeout) clearTimeout(longPressTimeout);
942
+ longPressTimeout = setTimeout(() => {
943
+ if (!hasMoved) {
944
+ removeTextOverlay(id);
945
+ }
946
+ }, 600);
909
947
  },
910
948
  onPanResponderMove: (_, gesture) => {
949
+ if (Math.abs(gesture.dx) > 4 || Math.abs(gesture.dy) > 4) {
950
+ hasMoved = true;
951
+ if (longPressTimeout) {
952
+ clearTimeout(longPressTimeout);
953
+ longPressTimeout = null;
954
+ }
955
+ }
956
+ const newX = startX + gesture.dx;
957
+ const newY = startY + gesture.dy;
958
+
959
+ // Trash zone: bottom 140px of screen, center horizontal
960
+ const isNearTrash = gesture.moveY > SCREEN_HEIGHT - 140 && Math.abs(gesture.moveX - SCREEN_WIDTH / 2) < 90;
961
+ setIsOverTrash(isNearTrash);
911
962
  setOverlays(prev => prev.map(o => o.id === id ? {
912
963
  ...o,
913
- x: startX + gesture.dx,
914
- y: startY + gesture.dy
964
+ x: newX,
965
+ y: newY
915
966
  } : o));
967
+ },
968
+ onPanResponderRelease: (_, gesture) => {
969
+ if (longPressTimeout) {
970
+ clearTimeout(longPressTimeout);
971
+ longPressTimeout = null;
972
+ }
973
+
974
+ // Check if released over trash zone using final touch screen coordinates
975
+ const releasedOverTrash = gesture.moveY > SCREEN_HEIGHT - 140 && Math.abs(gesture.moveX - SCREEN_WIDTH / 2) < 90;
976
+
977
+ // Reset dragging states
978
+ setActiveDraggingId(null);
979
+ setIsOverTrash(false);
980
+ if (releasedOverTrash) {
981
+ setTimeout(() => {
982
+ removeTextOverlay(id);
983
+ }, 50);
984
+ return;
985
+ }
986
+ const pressDuration = Date.now() - pressStartTime;
987
+ if (!hasMoved && pressDuration < 250) {
988
+ pushToHistory();
989
+ setOverlays(prev => {
990
+ const found = prev.find(o => o.id === id);
991
+ if (found) {
992
+ originalOverlayBackup.current = {
993
+ ...found
994
+ };
995
+ isNewOverlay.current = false;
996
+ setEditingTextId(id);
997
+ setNewText(found.text);
998
+ setPanel('text');
999
+ }
1000
+ return prev;
1001
+ });
1002
+ }
1003
+ },
1004
+ onPanResponderTerminate: () => {
1005
+ if (longPressTimeout) {
1006
+ clearTimeout(longPressTimeout);
1007
+ longPressTimeout = null;
1008
+ }
1009
+ setActiveDraggingId(null);
1010
+ setIsOverTrash(false);
916
1011
  }
917
1012
  });
918
1013
  };
@@ -1038,13 +1133,18 @@ export function EditorScreen({
1038
1133
  y: (o.y + 8) * renderScale,
1039
1134
  color: o.color,
1040
1135
  fontSize: o.fontSize * renderScale
1041
- }))
1136
+ })),
1137
+ frameUri: imageOptions.frame && FRAME_IMAGES[imageOptions.frame] ? Image.resolveAssetSource(FRAME_IMAGES[imageOptions.frame]).uri : undefined
1042
1138
  };
1043
1139
  }, [imageOptions, cropOffset, maxPan, dimensions, cropRatio, straightenAngle, overlays]);
1044
1140
 
1045
1141
  // For visual trim
1046
1142
 
1047
1143
  const duration = item.durationMs ?? 10_000;
1144
+ const durationRef = useRef(duration);
1145
+ useEffect(() => {
1146
+ durationRef.current = duration;
1147
+ }, [duration]);
1048
1148
  const formatTime = ms => {
1049
1149
  const totalSec = Math.floor(ms / 1000);
1050
1150
  const mins = Math.floor(totalSec / 60);
@@ -1106,6 +1206,39 @@ export function EditorScreen({
1106
1206
  generateThumbs();
1107
1207
  }
1108
1208
  }, [item.type, item.uri, duration, thumbnails.length]);
1209
+ const leftOverlayRef = useRef(null);
1210
+ const rightOverlayRef = useRef(null);
1211
+ const selectionRangeRef = useRef(null);
1212
+ const leftHandleRef = useRef(null);
1213
+ const rightHandleRef = useRef(null);
1214
+ const updateNativeRefs = (newStartX, newEndX) => {
1215
+ leftOverlayRef.current?.setNativeProps({
1216
+ style: {
1217
+ width: newStartX
1218
+ }
1219
+ });
1220
+ rightOverlayRef.current?.setNativeProps({
1221
+ style: {
1222
+ left: newEndX
1223
+ }
1224
+ });
1225
+ selectionRangeRef.current?.setNativeProps({
1226
+ style: {
1227
+ left: newStartX,
1228
+ width: newEndX - newStartX
1229
+ }
1230
+ });
1231
+ leftHandleRef.current?.setNativeProps({
1232
+ style: {
1233
+ left: newStartX - 16
1234
+ }
1235
+ });
1236
+ rightHandleRef.current?.setNativeProps({
1237
+ style: {
1238
+ left: newEndX - 16
1239
+ }
1240
+ });
1241
+ };
1109
1242
  const startPanOffset = useRef(0);
1110
1243
  const startPan = useRef(PanResponder.create({
1111
1244
  onStartShouldSetPanResponder: () => true,
@@ -1119,20 +1252,76 @@ export function EditorScreen({
1119
1252
  setScrollEnabled(false);
1120
1253
  },
1121
1254
  onPanResponderMove: (_, gesture) => {
1122
- const newX = Math.max(0, Math.min(endX.current - 32, startPanOffset.current + gesture.dx));
1255
+ let newX = Math.max(0, Math.min(endX.current - 32, startPanOffset.current + gesture.dx));
1256
+ let newTime = newX / TIMELINE_WIDTH * durationRef.current;
1257
+ const currentTrimEnd = endX.current / TIMELINE_WIDTH * durationRef.current;
1258
+ if (maxVideoDurationMs && currentTrimEnd - newTime > maxVideoDurationMs) {
1259
+ newTime = currentTrimEnd - maxVideoDurationMs;
1260
+ newX = newTime / durationRef.current * TIMELINE_WIDTH;
1261
+ }
1123
1262
  startX.current = newX;
1124
- const newTime = newX / TIMELINE_WIDTH * duration;
1125
- setTrimStart(newTime);
1263
+ updateNativeRefs(newX, endX.current);
1126
1264
  throttledSeek(newTime);
1127
1265
  },
1128
1266
  onPanResponderRelease: () => {
1129
1267
  isDraggingHandle.current = false;
1130
1268
  setScrollEnabled(true);
1269
+ setTrimStart(startX.current / TIMELINE_WIDTH * durationRef.current);
1270
+ setTrimEnd(endX.current / TIMELINE_WIDTH * durationRef.current);
1271
+ setSeekToMs(-1);
1272
+ },
1273
+ onPanResponderTerminate: () => {
1274
+ isDraggingHandle.current = false;
1275
+ setScrollEnabled(true);
1276
+ setTrimStart(startX.current / TIMELINE_WIDTH * durationRef.current);
1277
+ setTrimEnd(endX.current / TIMELINE_WIDTH * durationRef.current);
1278
+ setSeekToMs(-1);
1279
+ }
1280
+ })).current;
1281
+ const middlePanOffsetStart = useRef(0);
1282
+ const middlePanOffsetEnd = useRef(0);
1283
+ const middlePan = useRef(PanResponder.create({
1284
+ onStartShouldSetPanResponder: () => true,
1285
+ onStartShouldSetPanResponderCapture: () => true,
1286
+ onMoveShouldSetPanResponder: () => true,
1287
+ onMoveShouldSetPanResponderCapture: () => true,
1288
+ onPanResponderGrant: () => {
1289
+ pushToHistory();
1290
+ middlePanOffsetStart.current = startX.current;
1291
+ middlePanOffsetEnd.current = endX.current;
1292
+ isDraggingHandle.current = true;
1293
+ setScrollEnabled(false);
1294
+ },
1295
+ onPanResponderMove: (_, gesture) => {
1296
+ const windowWidth = middlePanOffsetEnd.current - middlePanOffsetStart.current;
1297
+ let newStartX = middlePanOffsetStart.current + gesture.dx;
1298
+ let newEndX = middlePanOffsetEnd.current + gesture.dx;
1299
+ if (newStartX < 0) {
1300
+ newStartX = 0;
1301
+ newEndX = windowWidth;
1302
+ }
1303
+ if (newEndX > TIMELINE_WIDTH) {
1304
+ newEndX = TIMELINE_WIDTH;
1305
+ newStartX = TIMELINE_WIDTH - windowWidth;
1306
+ }
1307
+ startX.current = newStartX;
1308
+ endX.current = newEndX;
1309
+ const newStartTime = newStartX / TIMELINE_WIDTH * durationRef.current;
1310
+ updateNativeRefs(newStartX, newEndX);
1311
+ throttledSeek(newStartTime);
1312
+ },
1313
+ onPanResponderRelease: () => {
1314
+ isDraggingHandle.current = false;
1315
+ setScrollEnabled(true);
1316
+ setTrimStart(startX.current / TIMELINE_WIDTH * durationRef.current);
1317
+ setTrimEnd(endX.current / TIMELINE_WIDTH * durationRef.current);
1131
1318
  setSeekToMs(-1);
1132
1319
  },
1133
1320
  onPanResponderTerminate: () => {
1134
1321
  isDraggingHandle.current = false;
1135
1322
  setScrollEnabled(true);
1323
+ setTrimStart(startX.current / TIMELINE_WIDTH * durationRef.current);
1324
+ setTrimEnd(endX.current / TIMELINE_WIDTH * durationRef.current);
1136
1325
  setSeekToMs(-1);
1137
1326
  }
1138
1327
  })).current;
@@ -1149,20 +1338,29 @@ export function EditorScreen({
1149
1338
  setScrollEnabled(false);
1150
1339
  },
1151
1340
  onPanResponderMove: (_, gesture) => {
1152
- const newX = Math.min(TIMELINE_WIDTH, Math.max(startX.current + 32, endPanOffset.current + gesture.dx));
1341
+ let newX = Math.min(TIMELINE_WIDTH, Math.max(startX.current + 32, endPanOffset.current + gesture.dx));
1342
+ let newTime = newX / TIMELINE_WIDTH * durationRef.current;
1343
+ const currentTrimStart = startX.current / TIMELINE_WIDTH * durationRef.current;
1344
+ if (maxVideoDurationMs && newTime - currentTrimStart > maxVideoDurationMs) {
1345
+ newTime = currentTrimStart + maxVideoDurationMs;
1346
+ newX = newTime / durationRef.current * TIMELINE_WIDTH;
1347
+ }
1153
1348
  endX.current = newX;
1154
- const newTime = newX / TIMELINE_WIDTH * duration;
1155
- setTrimEnd(newTime);
1349
+ updateNativeRefs(startX.current, newX);
1156
1350
  throttledSeek(newTime);
1157
1351
  },
1158
1352
  onPanResponderRelease: () => {
1159
1353
  isDraggingHandle.current = false;
1160
1354
  setScrollEnabled(true);
1355
+ setTrimStart(startX.current / TIMELINE_WIDTH * durationRef.current);
1356
+ setTrimEnd(endX.current / TIMELINE_WIDTH * durationRef.current);
1161
1357
  setSeekToMs(-1);
1162
1358
  },
1163
1359
  onPanResponderTerminate: () => {
1164
1360
  isDraggingHandle.current = false;
1165
1361
  setScrollEnabled(true);
1362
+ setTrimStart(startX.current / TIMELINE_WIDTH * durationRef.current);
1363
+ setTrimEnd(endX.current / TIMELINE_WIDTH * durationRef.current);
1166
1364
  setSeekToMs(-1);
1167
1365
  }
1168
1366
  })).current;
@@ -1937,11 +2135,16 @@ export function EditorScreen({
1937
2135
  });
1938
2136
  }
1939
2137
  } else {
2138
+ const safeEndMs = Math.min(trimEnd, item.durationMs || 10000);
2139
+ const safeStartMs = Math.min(trimStart, Math.max(0, safeEndMs - 100));
2140
+ const isFullTrim = trimStart === 0 && trimEnd >= (item.durationMs || 10000);
1940
2141
  exportUri = await trimVideo(item.uri, {
1941
- startMs: trimStart,
1942
- endMs: trimEnd,
2142
+ startMs: safeStartMs,
2143
+ endMs: safeEndMs,
1943
2144
  mute: isMuted,
1944
- musicUri: selectedMusic?.url || undefined,
2145
+ ...(selectedMusic?.url ? {
2146
+ musicUri: selectedMusic.url
2147
+ } : {}),
1945
2148
  ...activeOptions
1946
2149
  });
1947
2150
  }
@@ -1958,6 +2161,7 @@ export function EditorScreen({
1958
2161
  setSaving(true);
1959
2162
  saveEditsForIndex(activeIndex);
1960
2163
  const updatedItems = [...items];
2164
+ let cumulativeMusicOffsetMs = 0;
1961
2165
  for (let i = 0; i < items.length; i++) {
1962
2166
  const targetItem = items[i];
1963
2167
  let edits = editsHistoryRef.current[targetItem.id];
@@ -1983,6 +2187,7 @@ export function EditorScreen({
1983
2187
  outUri = await trimVideo(outUri, {
1984
2188
  isImage: true,
1985
2189
  musicUri: selectedMusic.url,
2190
+ musicOffsetMs: cumulativeMusicOffsetMs,
1986
2191
  rotateDegrees: 0,
1987
2192
  flipX: false,
1988
2193
  flipY: false,
@@ -1997,12 +2202,20 @@ export function EditorScreen({
1997
2202
  uri: outUri,
1998
2203
  thumbnailUri: outUri
1999
2204
  };
2205
+ cumulativeMusicOffsetMs += 10000; // Images are 10s by default
2000
2206
  } else {
2207
+ const originalDuration = targetItem.durationMs || maxVideoDurationMs || 10000;
2208
+ const safeEndMs = Math.min(edits.trimEnd, originalDuration);
2209
+ const safeStartMs = Math.min(edits.trimStart, Math.max(0, safeEndMs - 100));
2210
+ const isFullTrim = edits.trimStart === 0 && edits.trimEnd >= originalDuration;
2001
2211
  const outUri = await trimVideo(targetItem.uri, {
2002
- startMs: edits.trimStart,
2003
- endMs: edits.trimEnd,
2212
+ startMs: safeStartMs,
2213
+ endMs: safeEndMs,
2004
2214
  mute: edits.isMuted,
2005
- musicUri: selectedMusic?.url || undefined,
2215
+ ...(selectedMusic?.url ? {
2216
+ musicUri: selectedMusic.url,
2217
+ musicOffsetMs: cumulativeMusicOffsetMs
2218
+ } : {}),
2006
2219
  ...opts
2007
2220
  });
2008
2221
  let newThumb = undefined;
@@ -2020,6 +2233,7 @@ export function EditorScreen({
2020
2233
  thumbnailUri: newThumb ? newThumb : targetItem.thumbnailUri,
2021
2234
  durationMs: newDuration
2022
2235
  };
2236
+ cumulativeMusicOffsetMs += newDuration;
2023
2237
  }
2024
2238
  } else {
2025
2239
  if (selectedMusic) {
@@ -2028,6 +2242,7 @@ export function EditorScreen({
2028
2242
  outUri = await trimVideo(targetItem.uri, {
2029
2243
  isImage: true,
2030
2244
  musicUri: selectedMusic.url,
2245
+ musicOffsetMs: cumulativeMusicOffsetMs,
2031
2246
  rotateDegrees: 0,
2032
2247
  flipX: false,
2033
2248
  flipY: false,
@@ -2036,12 +2251,17 @@ export function EditorScreen({
2036
2251
  saturation: 1,
2037
2252
  grayscale: false
2038
2253
  });
2254
+ cumulativeMusicOffsetMs += 10000;
2039
2255
  } else {
2256
+ const safeEndMs = targetItem.durationMs || 10000;
2040
2257
  outUri = await trimVideo(targetItem.uri, {
2041
2258
  startMs: 0,
2042
- endMs: targetItem.durationMs || 10000,
2259
+ endMs: safeEndMs,
2043
2260
  mute: isMuted,
2044
- musicUri: selectedMusic.url,
2261
+ ...(selectedMusic?.url ? {
2262
+ musicUri: selectedMusic.url,
2263
+ musicOffsetMs: cumulativeMusicOffsetMs
2264
+ } : {}),
2045
2265
  rotateDegrees: 0,
2046
2266
  flipX: false,
2047
2267
  flipY: false,
@@ -2050,6 +2270,7 @@ export function EditorScreen({
2050
2270
  saturation: 1,
2051
2271
  grayscale: false
2052
2272
  });
2273
+ cumulativeMusicOffsetMs += safeEndMs;
2053
2274
  }
2054
2275
  updatedItems[i] = {
2055
2276
  ...targetItem,
@@ -2223,20 +2444,9 @@ export function EditorScreen({
2223
2444
  zIndex: 20
2224
2445
  }, isSelected && styles.selectedTextContainer],
2225
2446
  ...responder.panHandlers,
2226
- children: /*#__PURE__*/_jsx(Pressable, {
2227
- onLongPress: () => removeTextOverlay(overlay.id),
2228
- onPress: () => {
2229
- pushToHistory();
2230
- // Backup original state (text, color, position, size, etc.)
2231
- const found = overlays.find(o => o.id === overlay.id);
2232
- if (found) {
2233
- originalOverlayBackup.current = {
2234
- ...found
2235
- };
2236
- }
2237
- isNewOverlay.current = false;
2238
- setEditingTextId(overlay.id);
2239
- setNewText(overlay.text);
2447
+ children: /*#__PURE__*/_jsx(View, {
2448
+ style: {
2449
+ padding: 4
2240
2450
  },
2241
2451
  children: /*#__PURE__*/_jsx(Text, {
2242
2452
  style: [styles.textOverlay, {
@@ -2466,19 +2676,9 @@ export function EditorScreen({
2466
2676
  zIndex: 20
2467
2677
  }, isSelected && styles.selectedTextContainer],
2468
2678
  ...responder.panHandlers,
2469
- children: /*#__PURE__*/_jsx(Pressable, {
2470
- onLongPress: () => removeTextOverlay(overlay.id),
2471
- onPress: () => {
2472
- pushToHistory();
2473
- const found = overlays.find(o => o.id === overlay.id);
2474
- if (found) {
2475
- originalOverlayBackup.current = {
2476
- ...found
2477
- };
2478
- }
2479
- isNewOverlay.current = false;
2480
- setEditingTextId(overlay.id);
2481
- setNewText(overlay.text);
2679
+ children: /*#__PURE__*/_jsx(View, {
2680
+ style: {
2681
+ padding: 4
2482
2682
  },
2483
2683
  children: /*#__PURE__*/_jsx(Text, {
2484
2684
  style: [styles.textOverlay, {
@@ -2612,7 +2812,15 @@ export function EditorScreen({
2612
2812
  },
2613
2813
  style: styles.filmstripImage
2614
2814
  }, idx)), /*#__PURE__*/_jsx(View, {
2615
- style: styles.timelineOverlay
2815
+ style: [styles.timelineOverlay, {
2816
+ left: 0,
2817
+ width: trimStart / duration * TIMELINE_WIDTH
2818
+ }]
2819
+ }), /*#__PURE__*/_jsx(View, {
2820
+ style: [styles.timelineOverlay, {
2821
+ left: trimEnd / duration * TIMELINE_WIDTH,
2822
+ right: 0
2823
+ }]
2616
2824
  }), /*#__PURE__*/_jsx(View, {
2617
2825
  style: [styles.selectionRange, {
2618
2826
  left: trimStart / duration * TIMELINE_WIDTH,
@@ -2623,20 +2831,18 @@ export function EditorScreen({
2623
2831
  style: [styles.customHandle, styles.customHandleLeft, {
2624
2832
  left: trimStart / duration * TIMELINE_WIDTH - 16
2625
2833
  }],
2626
- ...startPan.panHandlers,
2627
2834
  children: /*#__PURE__*/_jsx(View, {
2628
2835
  style: styles.handleBarLine
2629
2836
  })
2630
2837
  }), /*#__PURE__*/_jsx(View, {
2631
2838
  style: [styles.customHandle, styles.customHandleRight, {
2632
- left: trimEnd / duration * TIMELINE_WIDTH
2839
+ left: trimEnd / duration * TIMELINE_WIDTH - 16
2633
2840
  }],
2634
- ...endPan.panHandlers,
2635
2841
  children: /*#__PURE__*/_jsx(View, {
2636
2842
  style: styles.handleBarLine
2637
2843
  })
2638
2844
  })]
2639
- }), /*#__PURE__*/_jsxs(Pressable, {
2845
+ }), resolvedMusicList.length > 0 && /*#__PURE__*/_jsxs(Pressable, {
2640
2846
  style: styles.subTrackRow,
2641
2847
  onPress: () => setShowMusicModal(true),
2642
2848
  children: [/*#__PURE__*/_jsx(Text, {
@@ -2917,7 +3123,7 @@ export function EditorScreen({
2917
3123
  style: styles.toolLabel,
2918
3124
  children: "Text"
2919
3125
  })]
2920
- }), /*#__PURE__*/_jsxs(Pressable, {
3126
+ }), resolvedMusicList.length > 0 && /*#__PURE__*/_jsxs(Pressable, {
2921
3127
  style: [styles.toolButton, showMusicModal && styles.toolButtonActive],
2922
3128
  onPress: () => setShowMusicModal(true),
2923
3129
  children: [/*#__PURE__*/_jsx(View, {
@@ -3057,19 +3263,9 @@ export function EditorScreen({
3057
3263
  zIndex: 20
3058
3264
  }, isSelected && styles.selectedTextContainer],
3059
3265
  ...responder.panHandlers,
3060
- children: /*#__PURE__*/_jsx(Pressable, {
3061
- onLongPress: () => removeTextOverlay(overlay.id),
3062
- onPress: () => {
3063
- pushToHistory();
3064
- const found = overlays.find(o => o.id === overlay.id);
3065
- if (found) {
3066
- originalOverlayBackup.current = {
3067
- ...found
3068
- };
3069
- }
3070
- isNewOverlay.current = false;
3071
- setEditingTextId(overlay.id);
3072
- setNewText(overlay.text);
3266
+ children: /*#__PURE__*/_jsx(View, {
3267
+ style: {
3268
+ padding: 4
3073
3269
  },
3074
3270
  children: /*#__PURE__*/_jsx(Text, {
3075
3271
  style: [styles.textOverlay, {
@@ -3089,7 +3285,7 @@ export function EditorScreen({
3089
3285
  contentContainerStyle: [styles.toolButtonsRow, {
3090
3286
  flexGrow: 1
3091
3287
  }],
3092
- children: [/*#__PURE__*/_jsxs(Pressable, {
3288
+ children: [resolvedMusicList.length > 0 && /*#__PURE__*/_jsxs(Pressable, {
3093
3289
  style: [styles.toolButton, showMusicModal && styles.toolButtonActive],
3094
3290
  onPress: () => setShowMusicModal(true),
3095
3291
  children: [/*#__PURE__*/_jsx(View, {
@@ -3258,6 +3454,12 @@ export function EditorScreen({
3258
3454
  data: items,
3259
3455
  extraData: [overlays, editingTextId, editsHistory, activeFilter, imageOptions, trimStart, trimEnd, isMuted, activeIndex],
3260
3456
  keyExtractor: it => it.id,
3457
+ initialScrollIndex: activeIndex,
3458
+ getItemLayout: (_, index) => ({
3459
+ length: SCREEN_WIDTH,
3460
+ offset: SCREEN_WIDTH * index,
3461
+ index
3462
+ }),
3261
3463
  horizontal: true,
3262
3464
  pagingEnabled: false,
3263
3465
  showsHorizontalScrollIndicator: false,
@@ -3368,7 +3570,7 @@ export function EditorScreen({
3368
3570
  fontWeight: '700'
3369
3571
  },
3370
3572
  children: (() => {
3371
- const selMs = trimEnd - trimStart;
3573
+ const selMs = (endX.current - startX.current) / TIMELINE_WIDTH * duration;
3372
3574
  const totalSec = Math.floor(selMs / 1000);
3373
3575
  return totalSec >= 60 ? `${Math.floor(totalSec / 60)}:${(totalSec % 60).toString().padStart(2, '0')} selected` : `${(selMs / 1000).toFixed(1)}s selected`;
3374
3576
  })()
@@ -3379,30 +3581,54 @@ export function EditorScreen({
3379
3581
  }],
3380
3582
  children: [/*#__PURE__*/_jsxs(View, {
3381
3583
  style: styles.filmstrip,
3382
- children: [thumbnails.map((uri, idx) => /*#__PURE__*/_jsx(Image, {
3584
+ children: [thumbnails.length === 0 ? /*#__PURE__*/_jsx(View, {
3585
+ style: {
3586
+ flex: 1,
3587
+ justifyContent: 'center',
3588
+ alignItems: 'center'
3589
+ },
3590
+ children: /*#__PURE__*/_jsx(ActivityIndicator, {
3591
+ color: "#ffffff",
3592
+ size: "small"
3593
+ })
3594
+ }) : thumbnails.map((uri, idx) => /*#__PURE__*/_jsx(Image, {
3383
3595
  source: {
3384
3596
  uri
3385
3597
  },
3386
3598
  style: styles.filmstripImage
3387
3599
  }, idx)), /*#__PURE__*/_jsx(View, {
3388
- style: styles.timelineOverlay
3600
+ ref: leftOverlayRef,
3601
+ style: [styles.timelineOverlay, {
3602
+ left: 0,
3603
+ width: startX.current
3604
+ }]
3389
3605
  }), /*#__PURE__*/_jsx(View, {
3390
- style: [styles.selectionRange, {
3391
- left: trimStart / duration * TIMELINE_WIDTH,
3392
- width: (trimEnd - trimStart) / duration * TIMELINE_WIDTH
3606
+ ref: rightOverlayRef,
3607
+ style: [styles.timelineOverlay, {
3608
+ left: endX.current,
3609
+ right: 0
3393
3610
  }]
3611
+ }), /*#__PURE__*/_jsx(View, {
3612
+ ref: selectionRangeRef,
3613
+ style: [styles.selectionRange, {
3614
+ left: startX.current,
3615
+ width: endX.current - startX.current
3616
+ }],
3617
+ ...middlePan.panHandlers
3394
3618
  })]
3395
3619
  }), /*#__PURE__*/_jsx(View, {
3620
+ ref: leftHandleRef,
3396
3621
  style: [styles.customHandle, styles.customHandleLeft, {
3397
- left: trimStart / duration * TIMELINE_WIDTH - 16
3622
+ left: startX.current - 16
3398
3623
  }],
3399
3624
  ...startPan.panHandlers,
3400
3625
  children: /*#__PURE__*/_jsx(View, {
3401
3626
  style: styles.handleBarLine
3402
3627
  })
3403
3628
  }), /*#__PURE__*/_jsx(View, {
3629
+ ref: rightHandleRef,
3404
3630
  style: [styles.customHandle, styles.customHandleRight, {
3405
- left: trimEnd / duration * TIMELINE_WIDTH
3631
+ left: endX.current - 16
3406
3632
  }],
3407
3633
  ...endPan.panHandlers,
3408
3634
  children: /*#__PURE__*/_jsx(View, {
@@ -3680,7 +3906,7 @@ export function EditorScreen({
3680
3906
  contentContainerStyle: [styles.toolButtonsRow, {
3681
3907
  flexGrow: 1
3682
3908
  }],
3683
- children: [/*#__PURE__*/_jsxs(Pressable, {
3909
+ children: [resolvedMusicList.length > 0 && /*#__PURE__*/_jsxs(Pressable, {
3684
3910
  style: [styles.toolButton, showMusicModal && styles.toolButtonActive],
3685
3911
  onPress: () => setShowMusicModal(true),
3686
3912
  children: [/*#__PURE__*/_jsx(View, {
@@ -3988,6 +4214,7 @@ export function EditorScreen({
3988
4214
  onPress: () => {
3989
4215
  setSelectedMusic(null);
3990
4216
  setMusicPaused(true);
4217
+ setIsMuted(false);
3991
4218
  },
3992
4219
  style: styles.musicFooterRemoveBtn,
3993
4220
  children: /*#__PURE__*/_jsx(Text, {
@@ -4097,6 +4324,24 @@ export function EditorScreen({
4097
4324
  })]
4098
4325
  })
4099
4326
  })
4327
+ }), activeDraggingId !== null && /*#__PURE__*/_jsx(View, {
4328
+ style: styles.globalTrashZone,
4329
+ pointerEvents: "none",
4330
+ children: /*#__PURE__*/_jsxs(View, {
4331
+ style: [styles.trashZoneContainer, isOverTrash && styles.trashZoneActive, isOverTrash ? {
4332
+ transform: [{
4333
+ scale: 1.15
4334
+ }]
4335
+ } : {}],
4336
+ children: [/*#__PURE__*/_jsx(Ionicons, {
4337
+ name: isOverTrash ? "trash" : "trash-outline",
4338
+ size: 20,
4339
+ color: "#fff"
4340
+ }), /*#__PURE__*/_jsx(Text, {
4341
+ style: styles.trashZoneText,
4342
+ children: isOverTrash ? "Release to Delete" : "Drag here to delete"
4343
+ })]
4344
+ })
4100
4345
  })]
4101
4346
  });
4102
4347
  }
@@ -4737,7 +4982,9 @@ const styles = StyleSheet.create({
4737
4982
  height: 60
4738
4983
  },
4739
4984
  timelineOverlay: {
4740
- ...StyleSheet.absoluteFillObject,
4985
+ position: 'absolute',
4986
+ top: 0,
4987
+ bottom: 0,
4741
4988
  backgroundColor: 'rgba(0,0,0,0.6)'
4742
4989
  },
4743
4990
  selectionRange: {
@@ -4747,7 +4994,7 @@ const styles = StyleSheet.create({
4747
4994
  backgroundColor: 'transparent',
4748
4995
  borderTopWidth: 2,
4749
4996
  borderBottomWidth: 2,
4750
- borderColor: '#fff'
4997
+ borderColor: '#FFD60A'
4751
4998
  },
4752
4999
  handle: {
4753
5000
  position: 'absolute',
@@ -4773,7 +5020,7 @@ const styles = StyleSheet.create({
4773
5020
  top: 0,
4774
5021
  width: 16,
4775
5022
  height: 60,
4776
- backgroundColor: '#FFFFFF',
5023
+ backgroundColor: '#FFD60A',
4777
5024
  justifyContent: 'center',
4778
5025
  alignItems: 'center',
4779
5026
  zIndex: 20
@@ -5375,6 +5622,44 @@ const styles = StyleSheet.create({
5375
5622
  borderWidth: 1,
5376
5623
  borderColor: 'rgba(255, 255, 255, 0.3)'
5377
5624
  },
5625
+ globalTrashZone: {
5626
+ position: 'absolute',
5627
+ bottom: Platform.OS === 'ios' ? 48 : 28,
5628
+ left: 0,
5629
+ right: 0,
5630
+ alignItems: 'center',
5631
+ justifyContent: 'center',
5632
+ zIndex: 9999
5633
+ },
5634
+ trashZoneContainer: {
5635
+ backgroundColor: 'rgba(0, 0, 0, 0.75)',
5636
+ paddingHorizontal: 20,
5637
+ paddingVertical: 12,
5638
+ borderRadius: 24,
5639
+ flexDirection: 'row',
5640
+ alignItems: 'center',
5641
+ justifyContent: 'center',
5642
+ borderWidth: 1.5,
5643
+ borderColor: 'rgba(255, 255, 255, 0.25)',
5644
+ shadowColor: '#000',
5645
+ shadowOffset: {
5646
+ width: 0,
5647
+ height: 4
5648
+ },
5649
+ shadowOpacity: 0.4,
5650
+ shadowRadius: 6,
5651
+ elevation: 8
5652
+ },
5653
+ trashZoneActive: {
5654
+ backgroundColor: '#ef4444',
5655
+ borderColor: '#fca5a5'
5656
+ },
5657
+ trashZoneText: {
5658
+ color: '#ffffff',
5659
+ fontSize: 12,
5660
+ fontWeight: '700',
5661
+ marginLeft: 8
5662
+ },
5378
5663
  cropIconText: {
5379
5664
  color: '#FFFFFF',
5380
5665
  fontSize: 18,