@technotoil/image-video-editor 0.1.1 → 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 (35) 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/components/VideoEditor.js +100 -32
  10. package/lib/commonjs/components/VideoEditor.js.map +1 -1
  11. package/lib/commonjs/screens/CropScreen.js +5 -3
  12. package/lib/commonjs/screens/CropScreen.js.map +1 -1
  13. package/lib/commonjs/screens/EditorScreen.js +229 -44
  14. package/lib/commonjs/screens/EditorScreen.js.map +1 -1
  15. package/lib/commonjs/screens/PickScreen.js +214 -122
  16. package/lib/commonjs/screens/PickScreen.js.map +1 -1
  17. package/lib/module/components/VideoEditor.js +100 -33
  18. package/lib/module/components/VideoEditor.js.map +1 -1
  19. package/lib/module/screens/CropScreen.js +5 -3
  20. package/lib/module/screens/CropScreen.js.map +1 -1
  21. package/lib/module/screens/EditorScreen.js +229 -44
  22. package/lib/module/screens/EditorScreen.js.map +1 -1
  23. package/lib/module/screens/PickScreen.js +215 -123
  24. package/lib/module/screens/PickScreen.js.map +1 -1
  25. package/lib/typescript/src/components/VideoEditor.d.ts +10 -2
  26. package/lib/typescript/src/screens/CropScreen.d.ts +2 -1
  27. package/lib/typescript/src/screens/EditorScreen.d.ts +2 -1
  28. package/lib/typescript/src/screens/PickScreen.d.ts +4 -1
  29. package/lib/typescript/src/types.d.ts +1 -0
  30. package/package.json +4 -1
  31. package/src/components/VideoEditor.tsx +68 -11
  32. package/src/screens/CropScreen.tsx +8 -3
  33. package/src/screens/EditorScreen.tsx +227 -61
  34. package/src/screens/PickScreen.tsx +197 -119
  35. package/src/types.ts +1 -0
@@ -194,6 +194,7 @@ export function EditorScreen({
194
194
  onSaved,
195
195
  onOpenCrop,
196
196
  musicList,
197
+ maxVideoDurationMs,
197
198
  }: {
198
199
  items: MediaItem[];
199
200
  initialIndex?: number;
@@ -201,11 +202,16 @@ export function EditorScreen({
201
202
  onSaved: (updatedItems: MediaItem[]) => void;
202
203
  onOpenCrop: (item: MediaItem) => void;
203
204
  musicList?: MusicTrack[];
205
+ maxVideoDurationMs?: number;
204
206
  }) {
205
207
  const [activeIndex, setActiveIndex] = useState(initialIndex);
206
208
  const currentItem = items[activeIndex] || items[0];
207
209
  const item = currentItem; // Aliasing to 'item' for ease of compatibility
208
210
 
211
+ useEffect(() => {
212
+ setActiveIndex(initialIndex);
213
+ }, [initialIndex]);
214
+
209
215
  const [activeFilter, setActiveFilter] = useState('none');
210
216
  const [imageOptions, setImageOptions] = useState<ImageEditOptions>({
211
217
  rotateDegrees: 0,
@@ -216,14 +222,22 @@ export function EditorScreen({
216
222
  saturation: 1,
217
223
  grayscale: false,
218
224
  });
225
+ const [panel, setPanel] = useState<'filter' | 'edit' | 'trim' | 'transform' | 'frame' | 'text' | 'ar' | 'music' | 'sticker' | 'effects' | 'caption' | 'addclip'>(item.type === 'video' ? 'trim' : 'filter');
219
226
  const [trimStart, setTrimStart] = useState(0);
220
- 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
+ });
221
231
 
222
232
  useEffect(() => {
223
233
  setTrimStart(0);
224
- setTrimEnd(item.durationMs || 10000);
234
+ const end = item.durationMs || maxVideoDurationMs || 10000;
235
+ setTrimEnd(maxVideoDurationMs ? Math.min(end, maxVideoDurationMs) : end);
225
236
  setThumbnails([]);
226
- }, [item.id, item.durationMs]);
237
+ if (item.type === 'video' && maxVideoDurationMs && end > maxVideoDurationMs) {
238
+ setPanel('trim');
239
+ }
240
+ }, [item.id, item.durationMs, maxVideoDurationMs]);
227
241
 
228
242
  const [editsHistory, setEditsHistory] = useState<Record<string, any>>({});
229
243
  const editsHistoryRef = useRef<Record<string, any>>({});
@@ -281,7 +295,7 @@ export function EditorScreen({
281
295
  setCropOffset(saved.cropOffset);
282
296
  setZoomScale(saved.zoomScale);
283
297
  setStraightenAngle(saved.straightenAngle);
284
- setIsMuted(saved.isMuted);
298
+ setIsMuted(selectedMusic ? true : saved.isMuted);
285
299
  } else {
286
300
  setActiveFilter('none');
287
301
  setImageOptions({
@@ -294,13 +308,21 @@ export function EditorScreen({
294
308
  grayscale: false,
295
309
  });
296
310
  setTrimStart(0);
297
- setTrimEnd(targetItem.durationMs || 10000);
311
+ const end = targetItem.durationMs || 10000;
312
+ setTrimEnd(maxVideoDurationMs ? Math.min(end, maxVideoDurationMs) : end);
298
313
  setOverlays([]);
299
314
  setCropRatio(null);
300
315
  setCropOffset({ x: 0, y: 0 });
301
316
  setZoomScale(1);
302
317
  setStraightenAngle(0);
303
- 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');
304
326
  }
305
327
  };
306
328
 
@@ -362,13 +384,15 @@ export function EditorScreen({
362
384
  color: o.color,
363
385
  fontSize: o.fontSize * renderScale,
364
386
  })),
387
+ frameUri: edits.imageOptions.frame && FRAME_IMAGES[edits.imageOptions.frame]
388
+ ? Image.resolveAssetSource(FRAME_IMAGES[edits.imageOptions.frame]).uri
389
+ : undefined,
365
390
  };
366
391
  };
367
392
 
368
393
  const [saving, setSaving] = useState(false);
369
394
  const [videoPaused, setVideoPaused] = useState(false);
370
- const [panel, setPanel] = useState<'filter' | 'edit' | 'trim' | 'transform' | 'frame' | 'text' | 'ar' | 'music' | 'sticker' | 'effects' | 'caption' | 'addclip'>(item.type === 'video' ? 'trim' : 'filter');
371
- const resolvedMusicList = musicList || DUMMY_MUSIC_LIST;
395
+ const resolvedMusicList = musicList || [];
372
396
 
373
397
  const [selectedMusic, setSelectedMusic] = useState<MusicTrack | null>(null);
374
398
  const [musicPaused, setMusicPaused] = useState(false);
@@ -1065,12 +1089,19 @@ export function EditorScreen({
1065
1089
  color: o.color,
1066
1090
  fontSize: o.fontSize * renderScale,
1067
1091
  })),
1092
+ frameUri: imageOptions.frame && FRAME_IMAGES[imageOptions.frame]
1093
+ ? Image.resolveAssetSource(FRAME_IMAGES[imageOptions.frame]).uri
1094
+ : undefined,
1068
1095
  };
1069
1096
  }, [imageOptions, cropOffset, maxPan, dimensions, cropRatio, straightenAngle, overlays]);
1070
1097
 
1071
1098
  // For visual trim
1072
1099
 
1073
1100
  const duration = item.durationMs ?? 10_000;
1101
+ const durationRef = useRef(duration);
1102
+ useEffect(() => {
1103
+ durationRef.current = duration;
1104
+ }, [duration]);
1074
1105
 
1075
1106
  const formatTime = (ms: number) => {
1076
1107
  const totalSec = Math.floor(ms / 1000);
@@ -1138,6 +1169,20 @@ export function EditorScreen({
1138
1169
  }
1139
1170
  }, [item.type, item.uri, duration, thumbnails.length]);
1140
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
+
1141
1186
  const startPanOffset = useRef(0);
1142
1187
  const startPan = useRef(
1143
1188
  PanResponder.create({
@@ -1152,20 +1197,86 @@ export function EditorScreen({
1152
1197
  setScrollEnabled(false);
1153
1198
  },
1154
1199
  onPanResponderMove: (_, gesture) => {
1155
- 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
+
1156
1209
  startX.current = newX;
1157
- const newTime = (newX / TIMELINE_WIDTH) * duration;
1158
- setTrimStart(newTime);
1210
+ updateNativeRefs(newX, endX.current);
1159
1211
  throttledSeek(newTime);
1160
1212
  },
1161
1213
  onPanResponderRelease: () => {
1162
1214
  isDraggingHandle.current = false;
1163
1215
  setScrollEnabled(true);
1216
+ setTrimStart((startX.current / TIMELINE_WIDTH) * durationRef.current);
1217
+ setTrimEnd((endX.current / TIMELINE_WIDTH) * durationRef.current);
1218
+ setSeekToMs(-1);
1219
+ },
1220
+ onPanResponderTerminate: () => {
1221
+ isDraggingHandle.current = false;
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);
1164
1273
  setSeekToMs(-1);
1165
1274
  },
1166
1275
  onPanResponderTerminate: () => {
1167
1276
  isDraggingHandle.current = false;
1168
1277
  setScrollEnabled(true);
1278
+ setTrimStart((startX.current / TIMELINE_WIDTH) * durationRef.current);
1279
+ setTrimEnd((endX.current / TIMELINE_WIDTH) * durationRef.current);
1169
1280
  setSeekToMs(-1);
1170
1281
  }
1171
1282
  })
@@ -1185,20 +1296,31 @@ export function EditorScreen({
1185
1296
  setScrollEnabled(false);
1186
1297
  },
1187
1298
  onPanResponderMove: (_, gesture) => {
1188
- 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
+
1189
1308
  endX.current = newX;
1190
- const newTime = (newX / TIMELINE_WIDTH) * duration;
1191
- setTrimEnd(newTime);
1309
+ updateNativeRefs(startX.current, newX);
1192
1310
  throttledSeek(newTime);
1193
1311
  },
1194
1312
  onPanResponderRelease: () => {
1195
1313
  isDraggingHandle.current = false;
1196
1314
  setScrollEnabled(true);
1315
+ setTrimStart((startX.current / TIMELINE_WIDTH) * durationRef.current);
1316
+ setTrimEnd((endX.current / TIMELINE_WIDTH) * durationRef.current);
1197
1317
  setSeekToMs(-1);
1198
1318
  },
1199
1319
  onPanResponderTerminate: () => {
1200
1320
  isDraggingHandle.current = false;
1201
1321
  setScrollEnabled(true);
1322
+ setTrimStart((startX.current / TIMELINE_WIDTH) * durationRef.current);
1323
+ setTrimEnd((endX.current / TIMELINE_WIDTH) * durationRef.current);
1202
1324
  setSeekToMs(-1);
1203
1325
  }
1204
1326
  })
@@ -1501,11 +1623,15 @@ export function EditorScreen({
1501
1623
  });
1502
1624
  }
1503
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
+
1504
1630
  exportUri = await trimVideo(item.uri, {
1505
- startMs: trimStart,
1506
- endMs: trimEnd,
1631
+ startMs: safeStartMs,
1632
+ endMs: safeEndMs,
1507
1633
  mute: isMuted,
1508
- musicUri: selectedMusic?.url || undefined,
1634
+ ...(selectedMusic?.url ? { musicUri: selectedMusic.url } : {}),
1509
1635
  ...activeOptions,
1510
1636
  });
1511
1637
  }
@@ -1524,6 +1650,7 @@ export function EditorScreen({
1524
1650
  saveEditsForIndex(activeIndex);
1525
1651
 
1526
1652
  const updatedItems = [...items];
1653
+ let cumulativeMusicOffsetMs = 0;
1527
1654
 
1528
1655
  for (let i = 0; i < items.length; i++) {
1529
1656
  const targetItem = items[i];
@@ -1553,6 +1680,7 @@ export function EditorScreen({
1553
1680
  outUri = await trimVideo(outUri, {
1554
1681
  isImage: true,
1555
1682
  musicUri: selectedMusic.url,
1683
+ musicOffsetMs: cumulativeMusicOffsetMs,
1556
1684
  rotateDegrees: 0,
1557
1685
  flipX: false,
1558
1686
  flipY: false,
@@ -1567,12 +1695,18 @@ export function EditorScreen({
1567
1695
  uri: outUri,
1568
1696
  thumbnailUri: outUri,
1569
1697
  };
1698
+ cumulativeMusicOffsetMs += 10000; // Images are 10s by default
1570
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
+
1571
1705
  const outUri = await trimVideo(targetItem.uri, {
1572
- startMs: edits.trimStart,
1573
- endMs: edits.trimEnd,
1706
+ startMs: safeStartMs,
1707
+ endMs: safeEndMs,
1574
1708
  mute: edits.isMuted,
1575
- musicUri: selectedMusic?.url || undefined,
1709
+ ...(selectedMusic?.url ? { musicUri: selectedMusic.url, musicOffsetMs: cumulativeMusicOffsetMs } : {}),
1576
1710
  ...opts,
1577
1711
  });
1578
1712
 
@@ -1590,6 +1724,7 @@ export function EditorScreen({
1590
1724
  thumbnailUri: newThumb ? newThumb : targetItem.thumbnailUri,
1591
1725
  durationMs: newDuration,
1592
1726
  };
1727
+ cumulativeMusicOffsetMs += newDuration;
1593
1728
  }
1594
1729
  } else {
1595
1730
  if (selectedMusic) {
@@ -1598,6 +1733,7 @@ export function EditorScreen({
1598
1733
  outUri = await trimVideo(targetItem.uri, {
1599
1734
  isImage: true,
1600
1735
  musicUri: selectedMusic.url,
1736
+ musicOffsetMs: cumulativeMusicOffsetMs,
1601
1737
  rotateDegrees: 0,
1602
1738
  flipX: false,
1603
1739
  flipY: false,
@@ -1606,12 +1742,14 @@ export function EditorScreen({
1606
1742
  saturation: 1,
1607
1743
  grayscale: false,
1608
1744
  });
1745
+ cumulativeMusicOffsetMs += 10000;
1609
1746
  } else {
1747
+ const safeEndMs = targetItem.durationMs || 10000;
1610
1748
  outUri = await trimVideo(targetItem.uri, {
1611
1749
  startMs: 0,
1612
- endMs: targetItem.durationMs || 10000,
1750
+ endMs: safeEndMs,
1613
1751
  mute: isMuted,
1614
- musicUri: selectedMusic.url,
1752
+ ...(selectedMusic?.url ? { musicUri: selectedMusic.url, musicOffsetMs: cumulativeMusicOffsetMs } : {}),
1615
1753
  rotateDegrees: 0,
1616
1754
  flipX: false,
1617
1755
  flipY: false,
@@ -1620,6 +1758,7 @@ export function EditorScreen({
1620
1758
  saturation: 1,
1621
1759
  grayscale: false,
1622
1760
  });
1761
+ cumulativeMusicOffsetMs += safeEndMs;
1623
1762
  }
1624
1763
  updatedItems[i] = {
1625
1764
  ...targetItem,
@@ -2132,7 +2271,8 @@ export function EditorScreen({
2132
2271
  {thumbnails.map((uri, idx) => (
2133
2272
  <Image key={idx} source={{ uri }} style={styles.filmstripImage} />
2134
2273
  ))}
2135
- <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 }]} />
2136
2276
  <View
2137
2277
  style={[
2138
2278
  styles.selectionRange,
@@ -2146,7 +2286,6 @@ export function EditorScreen({
2146
2286
  styles.customHandleLeft,
2147
2287
  { left: (trimStart / duration) * TIMELINE_WIDTH - 16 }
2148
2288
  ]}
2149
- {...startPan.panHandlers}
2150
2289
  >
2151
2290
  <View style={styles.handleBarLine} />
2152
2291
  </View>
@@ -2154,21 +2293,22 @@ export function EditorScreen({
2154
2293
  style={[
2155
2294
  styles.customHandle,
2156
2295
  styles.customHandleRight,
2157
- { left: (trimEnd / duration) * TIMELINE_WIDTH }
2296
+ { left: (trimEnd / duration) * TIMELINE_WIDTH - 16 }
2158
2297
  ]}
2159
- {...endPan.panHandlers}
2160
2298
  >
2161
2299
  <View style={styles.handleBarLine} />
2162
2300
  </View>
2163
2301
  </View>
2164
2302
 
2165
2303
  {/* Sub-track 1: Add Audio */}
2166
- <Pressable style={styles.subTrackRow} onPress={() => setShowMusicModal(true)}>
2167
- <Text style={styles.subTrackIcon}>+</Text>
2168
- <Text style={styles.subTrackText}>
2169
- {selectedMusic ? `Audio: ${selectedMusic.title}` : 'Add audio'}
2170
- </Text>
2171
- </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
+ )}
2172
2312
 
2173
2313
  {/* Sub-track 2: Add Text */}
2174
2314
  <Pressable style={styles.subTrackRow} onPress={addTextOverlay}>
@@ -2309,12 +2449,14 @@ export function EditorScreen({
2309
2449
  </View>
2310
2450
  <Text style={styles.toolLabel}>Text</Text>
2311
2451
  </Pressable>
2312
- <Pressable style={[styles.toolButton, showMusicModal && styles.toolButtonActive]} onPress={() => setShowMusicModal(true)}>
2313
- <View style={styles.toolIconContainer}>
2314
- <Ionicons name="musical-notes" size={22} color="#fff" />
2315
- </View>
2316
- <Text style={styles.toolLabel}>Audio</Text>
2317
- </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
+ )}
2318
2460
  <Pressable style={[styles.toolButton, panel === 'transform' && styles.toolButtonActive]} onPress={() => setPanel(panel === 'transform' ? 'trim' : 'transform')}>
2319
2461
  <View style={styles.toolIconContainer}>
2320
2462
  <Ionicons name="crop" size={22} color="#fff" />
@@ -2420,12 +2562,14 @@ export function EditorScreen({
2420
2562
  <View style={styles.fullscreenBottomContainer}>
2421
2563
  {/* Bottom Toolbar */}
2422
2564
  <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={[styles.toolButtonsRow, { flexGrow: 1 }]}>
2423
- <Pressable style={[styles.toolButton, showMusicModal && styles.toolButtonActive]} onPress={() => setShowMusicModal(true)}>
2424
- <View style={styles.toolIconContainer}>
2425
- <Ionicons name="musical-notes" size={22} color="#fff" />
2426
- </View>
2427
- <Text style={styles.toolLabel}>Audio</Text>
2428
- </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
+ )}
2429
2573
  <Pressable style={styles.toolButton} onPress={addTextOverlay}>
2430
2574
  <View style={styles.toolIconContainer}>
2431
2575
  <Ionicons name="text" size={22} color="#fff" />
@@ -2511,6 +2655,12 @@ export function EditorScreen({
2511
2655
  activeIndex,
2512
2656
  ]}
2513
2657
  keyExtractor={(it) => it.id}
2658
+ initialScrollIndex={activeIndex}
2659
+ getItemLayout={(_, index) => ({
2660
+ length: SCREEN_WIDTH,
2661
+ offset: SCREEN_WIDTH * index,
2662
+ index,
2663
+ })}
2514
2664
  horizontal
2515
2665
  pagingEnabled={false}
2516
2666
  showsHorizontalScrollIndicator={false}
@@ -2590,7 +2740,7 @@ export function EditorScreen({
2590
2740
  <View style={{ alignItems: 'center', marginBottom: 6 }}>
2591
2741
  <Text style={{ color: '#fff', fontSize: 13, fontWeight: '700' }}>
2592
2742
  {(() => {
2593
- const selMs = trimEnd - trimStart;
2743
+ const selMs = ((endX.current - startX.current) / TIMELINE_WIDTH) * duration;
2594
2744
  const totalSec = Math.floor(selMs / 1000);
2595
2745
  return totalSec >= 60
2596
2746
  ? `${Math.floor(totalSec / 60)}:${(totalSec % 60).toString().padStart(2, '0')} selected`
@@ -2600,32 +2750,43 @@ export function EditorScreen({
2600
2750
  </View>
2601
2751
  <View style={[styles.trimTimelineBox, { overflow: 'visible' }]}>
2602
2752
  <View style={styles.filmstrip}>
2603
- {thumbnails.map((uri, idx) => (
2604
- <Image key={idx} source={{ uri }} style={styles.filmstripImage} />
2605
- ))}
2606
- <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 }]} />
2607
2764
  <View
2765
+ ref={selectionRangeRef}
2608
2766
  style={[
2609
2767
  styles.selectionRange,
2610
- { left: (trimStart / duration) * TIMELINE_WIDTH, width: ((trimEnd - trimStart) / duration) * TIMELINE_WIDTH }
2768
+ { left: startX.current, width: endX.current - startX.current }
2611
2769
  ]}
2770
+ {...middlePan.panHandlers}
2612
2771
  />
2613
2772
  </View>
2614
2773
  <View
2774
+ ref={leftHandleRef}
2615
2775
  style={[
2616
2776
  styles.customHandle,
2617
2777
  styles.customHandleLeft,
2618
- { left: (trimStart / duration) * TIMELINE_WIDTH - 16 }
2778
+ { left: startX.current - 16 }
2619
2779
  ]}
2620
2780
  {...startPan.panHandlers}
2621
2781
  >
2622
2782
  <View style={styles.handleBarLine} />
2623
2783
  </View>
2624
2784
  <View
2785
+ ref={rightHandleRef}
2625
2786
  style={[
2626
2787
  styles.customHandle,
2627
2788
  styles.customHandleRight,
2628
- { left: (trimEnd / duration) * TIMELINE_WIDTH }
2789
+ { left: endX.current - 16 }
2629
2790
  ]}
2630
2791
  {...endPan.panHandlers}
2631
2792
  >
@@ -2810,12 +2971,14 @@ export function EditorScreen({
2810
2971
  {/* Tools row and bottom navigation controls */}
2811
2972
  <View style={styles.bottomToolBarContainer}>
2812
2973
  <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={[styles.toolButtonsRow, { flexGrow: 1 }]}>
2813
- <Pressable style={[styles.toolButton, showMusicModal && styles.toolButtonActive]} onPress={() => setShowMusicModal(true)}>
2814
- <View style={styles.toolIconContainer}>
2815
- <Ionicons name="musical-notes" size={22} color="#fff" />
2816
- </View>
2817
- <Text style={styles.toolLabel}>Audio</Text>
2818
- </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
+ )}
2819
2982
  <Pressable style={[styles.toolButton, panel === 'text' && styles.toolButtonActive]} onPress={() => setPanel('text')}>
2820
2983
  <View style={styles.toolIconContainer}>
2821
2984
  <Ionicons name="text" size={22} color="#fff" />
@@ -3008,6 +3171,7 @@ export function EditorScreen({
3008
3171
  onPress={() => {
3009
3172
  setSelectedMusic(null);
3010
3173
  setMusicPaused(true);
3174
+ setIsMuted(false);
3011
3175
  }}
3012
3176
  style={styles.musicFooterRemoveBtn}
3013
3177
  >
@@ -3651,7 +3815,9 @@ const styles = StyleSheet.create({
3651
3815
  },
3652
3816
  filmstripImage: { width: TIMELINE_WIDTH / 10, height: 60 },
3653
3817
  timelineOverlay: {
3654
- ...StyleSheet.absoluteFillObject,
3818
+ position: 'absolute',
3819
+ top: 0,
3820
+ bottom: 0,
3655
3821
  backgroundColor: 'rgba(0,0,0,0.6)',
3656
3822
  },
3657
3823
  selectionRange: {
@@ -3661,7 +3827,7 @@ const styles = StyleSheet.create({
3661
3827
  backgroundColor: 'transparent',
3662
3828
  borderTopWidth: 2,
3663
3829
  borderBottomWidth: 2,
3664
- borderColor: '#fff',
3830
+ borderColor: '#FFD60A',
3665
3831
  },
3666
3832
  handle: {
3667
3833
  position: 'absolute',
@@ -3684,7 +3850,7 @@ const styles = StyleSheet.create({
3684
3850
  top: 0,
3685
3851
  width: 16,
3686
3852
  height: 60,
3687
- backgroundColor: '#FFFFFF',
3853
+ backgroundColor: '#FFD60A',
3688
3854
  justifyContent: 'center',
3689
3855
  alignItems: 'center',
3690
3856
  zIndex: 20,