@technotoil/image-video-editor 0.1.1 → 0.1.3
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.
- package/README.md +17 -3
- package/android/src/main/java/com/technotoil/image_videoeditor/FrameGrabberModule.kt +2 -6
- package/android/src/main/java/com/technotoil/image_videoeditor/MediaEditorModule.kt +103 -37
- package/android/src/main/java/com/technotoil/image_videoeditor/MediaLibraryModule.kt +51 -35
- package/android/src/main/java/com/technotoil/image_videoeditor/RNVideoPreviewManager.kt +98 -117
- package/ios/RNMediaEditor.m +38 -7
- package/ios/RNMediaLibrary.m +19 -15
- package/ios/RNVideoPreviewManager.m +2 -0
- package/lib/commonjs/components/Icon.js +31 -0
- package/lib/commonjs/components/Icon.js.map +1 -0
- package/lib/commonjs/components/VideoEditor.js +97 -32
- package/lib/commonjs/components/VideoEditor.js.map +1 -1
- package/lib/commonjs/icons.js +35 -0
- package/lib/commonjs/icons.js.map +1 -0
- package/lib/commonjs/screens/CropScreen.js +5 -3
- package/lib/commonjs/screens/CropScreen.js.map +1 -1
- package/lib/commonjs/screens/EditorScreen.js +269 -84
- package/lib/commonjs/screens/EditorScreen.js.map +1 -1
- package/lib/commonjs/screens/PickScreen.js +221 -129
- package/lib/commonjs/screens/PickScreen.js.map +1 -1
- package/lib/module/components/Icon.js +25 -0
- package/lib/module/components/Icon.js.map +1 -0
- package/lib/module/components/VideoEditor.js +98 -33
- package/lib/module/components/VideoEditor.js.map +1 -1
- package/lib/module/icons.js +31 -0
- package/lib/module/icons.js.map +1 -0
- package/lib/module/screens/CropScreen.js +5 -3
- package/lib/module/screens/CropScreen.js.map +1 -1
- package/lib/module/screens/EditorScreen.js +230 -45
- package/lib/module/screens/EditorScreen.js.map +1 -1
- package/lib/module/screens/PickScreen.js +216 -124
- package/lib/module/screens/PickScreen.js.map +1 -1
- package/lib/typescript/src/components/Icon.d.ts +11 -0
- package/lib/typescript/src/components/VideoEditor.d.ts +10 -2
- package/lib/typescript/src/icons.d.ts +28 -0
- package/lib/typescript/src/screens/CropScreen.d.ts +2 -1
- package/lib/typescript/src/screens/EditorScreen.d.ts +2 -1
- package/lib/typescript/src/screens/PickScreen.d.ts +4 -1
- package/lib/typescript/src/types.d.ts +1 -0
- package/package.json +4 -4
- package/src/components/Icon.tsx +19 -0
- package/src/components/VideoEditor.tsx +67 -11
- package/src/icons.ts +28 -0
- package/src/screens/CropScreen.tsx +8 -3
- package/src/screens/EditorScreen.tsx +228 -62
- package/src/screens/PickScreen.tsx +198 -120
- package/src/types.ts +1 -0
|
@@ -20,7 +20,7 @@ import { captureFrame } from '../native/FrameGrabber';
|
|
|
20
20
|
import { saveToGallery } from '../native/MediaLibrary';
|
|
21
21
|
import { VideoPreview } from '../native/VideoPreview';
|
|
22
22
|
import Video from 'react-native-video';
|
|
23
|
-
import Ionicons from '
|
|
23
|
+
import { Icon as Ionicons } from '../components/Icon';
|
|
24
24
|
import type { ImageEditOptions, MediaItem, MusicTrack } from '../types';
|
|
25
25
|
|
|
26
26
|
|
|
@@ -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(
|
|
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
|
-
|
|
234
|
+
const end = item.durationMs || maxVideoDurationMs || 10000;
|
|
235
|
+
setTrimEnd(maxVideoDurationMs ? Math.min(end, maxVideoDurationMs) : end);
|
|
225
236
|
setThumbnails([]);
|
|
226
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
1506
|
-
endMs:
|
|
1631
|
+
startMs: safeStartMs,
|
|
1632
|
+
endMs: safeEndMs,
|
|
1507
1633
|
mute: isMuted,
|
|
1508
|
-
musicUri: selectedMusic
|
|
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:
|
|
1573
|
-
endMs:
|
|
1706
|
+
startMs: safeStartMs,
|
|
1707
|
+
endMs: safeEndMs,
|
|
1574
1708
|
mute: edits.isMuted,
|
|
1575
|
-
musicUri: selectedMusic
|
|
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:
|
|
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
|
-
|
|
2167
|
-
<
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
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
|
-
|
|
2313
|
-
<
|
|
2314
|
-
<
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
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
|
-
|
|
2424
|
-
<
|
|
2425
|
-
<
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
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 =
|
|
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.
|
|
2604
|
-
<
|
|
2605
|
-
|
|
2606
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
2814
|
-
<
|
|
2815
|
-
<
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
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
|
-
|
|
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: '#
|
|
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: '#
|
|
3853
|
+
backgroundColor: '#FFD60A',
|
|
3688
3854
|
justifyContent: 'center',
|
|
3689
3855
|
alignItems: 'center',
|
|
3690
3856
|
zIndex: 20,
|