@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.
- 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 +75 -35
- 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/assets/frames/film_vintage.png +0 -0
- package/lib/commonjs/assets/frames/floral_gold.png +0 -0
- package/lib/commonjs/assets/frames/minimal_double.png +0 -0
- package/lib/commonjs/assets/frames/polaroid_white.png +0 -0
- package/lib/commonjs/assets/frames/watercolor_floral.png +0 -0
- package/lib/commonjs/components/VideoEditor.js +235 -0
- package/lib/commonjs/components/VideoEditor.js.map +1 -0
- package/lib/commonjs/index.js +14 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/native/CameraView.js +109 -0
- package/lib/commonjs/native/CameraView.js.map +1 -0
- package/lib/commonjs/native/FrameGrabber.js +17 -0
- package/lib/commonjs/native/FrameGrabber.js.map +1 -0
- package/lib/commonjs/native/MediaEditor.js +24 -0
- package/lib/commonjs/native/MediaEditor.js.map +1 -0
- package/lib/commonjs/native/MediaLibrary.js +45 -0
- package/lib/commonjs/native/MediaLibrary.js.map +1 -0
- package/lib/commonjs/native/MediaPicker.js +17 -0
- package/lib/commonjs/native/MediaPicker.js.map +1 -0
- package/lib/commonjs/native/MediaPlayer.js +17 -0
- package/lib/commonjs/native/MediaPlayer.js.map +1 -0
- package/lib/commonjs/native/VideoPreview.js +17 -0
- package/lib/commonjs/native/VideoPreview.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/screens/CropScreen.js +1233 -0
- package/lib/commonjs/screens/CropScreen.js.map +1 -0
- package/lib/commonjs/screens/EditorScreen.js +6043 -0
- package/lib/commonjs/screens/EditorScreen.js.map +1 -0
- package/lib/commonjs/screens/ExportScreen.js +294 -0
- package/lib/commonjs/screens/ExportScreen.js.map +1 -0
- package/lib/commonjs/screens/GalleryScreen.js +510 -0
- package/lib/commonjs/screens/GalleryScreen.js.map +1 -0
- package/lib/commonjs/screens/PickScreen.js +1353 -0
- package/lib/commonjs/screens/PickScreen.js.map +1 -0
- package/lib/commonjs/types.js +2 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/module/components/VideoEditor.js +104 -31
- package/lib/module/components/VideoEditor.js.map +1 -1
- package/lib/module/screens/CropScreen.js +26 -9
- package/lib/module/screens/CropScreen.js.map +1 -1
- package/lib/module/screens/EditorScreen.js +371 -86
- package/lib/module/screens/EditorScreen.js.map +1 -1
- package/lib/module/screens/PickScreen.js +245 -93
- package/lib/module/screens/PickScreen.js.map +1 -1
- package/lib/typescript/src/components/VideoEditor.d.ts +18 -2
- package/lib/typescript/src/screens/CropScreen.d.ts +3 -1
- package/lib/typescript/src/screens/EditorScreen.d.ts +2 -1
- package/lib/typescript/src/screens/PickScreen.d.ts +6 -1
- package/lib/typescript/src/types.d.ts +1 -0
- package/package.json +17 -8
- package/src/components/VideoEditor.tsx +82 -11
- package/src/screens/CropScreen.tsx +54 -33
- package/src/screens/EditorScreen.tsx +366 -106
- package/src/screens/PickScreen.tsx +231 -76
- 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(
|
|
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
|
-
|
|
234
|
+
const end = item.durationMs || maxVideoDurationMs || 10000;
|
|
235
|
+
setTrimEnd(maxVideoDurationMs ? Math.min(end, maxVideoDurationMs) : end);
|
|
224
236
|
setThumbnails([]);
|
|
225
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
846
|
-
y:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
1427
|
-
endMs:
|
|
1631
|
+
startMs: safeStartMs,
|
|
1632
|
+
endMs: safeEndMs,
|
|
1428
1633
|
mute: isMuted,
|
|
1429
|
-
musicUri: selectedMusic
|
|
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:
|
|
1494
|
-
endMs:
|
|
1706
|
+
startMs: safeStartMs,
|
|
1707
|
+
endMs: safeEndMs,
|
|
1495
1708
|
mute: edits.isMuted,
|
|
1496
|
-
musicUri: selectedMusic
|
|
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:
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
|
|
2113
|
-
<
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
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
|
-
|
|
2259
|
-
<
|
|
2260
|
-
<
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
|
|
2382
|
-
<
|
|
2383
|
-
<
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
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 =
|
|
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.
|
|
2562
|
-
<
|
|
2563
|
-
|
|
2564
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
2772
|
-
<
|
|
2773
|
-
<
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
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
|
-
|
|
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: '#
|
|
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: '#
|
|
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,
|