diff --git a/src/InverseCutSegment.jsx b/src/InverseCutSegment.jsx
new file mode 100644
index 00000000..9abcddc0
--- /dev/null
+++ b/src/InverseCutSegment.jsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import { motion } from 'framer-motion';
+import { FaTrashAlt, FaSave } from 'react-icons/fa';
+
+import { mySpring } from './animations';
+
+const InverseCutSegment = ({ seg, duration, invertCutSegments }) => (
+
+
+ {invertCutSegments ? (
+
+ ) : (
+
+ )}
+
+
+);
+
+export default InverseCutSegment;
diff --git a/src/TimelineSeg.jsx b/src/TimelineSeg.jsx
index 93fc34e0..a3be422e 100644
--- a/src/TimelineSeg.jsx
+++ b/src/TimelineSeg.jsx
@@ -1,68 +1,92 @@
import React from 'react';
-import { motion } from 'framer-motion';
+import { motion, AnimatePresence } from 'framer-motion';
+import { FaTrashAlt } from 'react-icons/fa';
+
+import { mySpring } from './animations';
const { formatDuration } = require('./util');
const TimelineSeg = ({
- duration, apparentCutStart, apparentCutEnd, isActive, segNum,
- onSegClick, color,
+ duration, cutStart, cutEnd, isActive, segNum,
+ onSegClick, color, invertCutSegments,
}) => {
const markerWidth = 4;
- const cutSectionWidth = `${Math.max(((apparentCutEnd - apparentCutStart) / duration) * 100, 1)}%`;
+ const cutSectionWidth = `${Math.max(((cutEnd - cutStart) / duration) * 100, 1)}%`;
- const startTimePos = `${(apparentCutStart / duration) * 100}%`;
- const markerBorder = isActive ? `2px solid ${color.lighten(0.5).string()}` : undefined;
- const backgroundColor = isActive ? color.lighten(0.5).alpha(0.5).string() : color.alpha(0.5).string();
+ const strongColor = color.lighten(0.5).string();
+ const strongBgColor = color.lighten(0.5).alpha(0.5).string();
+ const startTimePos = `${(cutStart / duration) * 100}%`;
+ const markerBorder = isActive ? `2px solid ${strongColor}` : undefined;
+ const backgroundColor = isActive ? strongBgColor : color.alpha(0.5).string();
const markerBorderRadius = 5;
+ const wrapperStyle = {
+ position: 'absolute',
+ top: 0,
+ bottom: 0,
+ left: startTimePos,
+ width: cutSectionWidth,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ background: backgroundColor,
+ originX: 0,
+ borderRadius: markerBorderRadius,
+ };
+
const startMarkerStyle = {
+ height: '100%',
width: markerWidth,
borderLeft: markerBorder,
borderTopLeftRadius: markerBorderRadius,
borderBottomLeftRadius: markerBorderRadius,
};
const endMarkerStyle = {
+ height: '100%',
width: markerWidth,
borderRight: markerBorder,
borderTopRightRadius: markerBorderRadius,
borderBottomRightRadius: markerBorderRadius,
};
- const wrapperStyle = {
- position: 'absolute',
- top: 0,
- bottom: 0,
- left: startTimePos,
- width: cutSectionWidth,
- display: 'flex',
- background: backgroundColor,
- originX: 0,
- borderRadius: markerBorderRadius,
- };
-
const onThisSegClick = () => onSegClick(segNum);
return (
cutStart ? formatDuration({ seconds: cutEnd - cutStart }) : undefined}
>
-
+
+
+ {segNum + 1}
+
+
+ {invertCutSegments && (
+
+
+
+ )}
+
+
+
- apparentCutStart ? formatDuration({ seconds: apparentCutEnd - apparentCutStart }) : undefined}
- >
- {segNum + 1}
-
- {apparentCutEnd > apparentCutStart && (
-
+ {cutEnd > cutStart && (
+
)}
);
diff --git a/src/animations.js b/src/animations.js
new file mode 100644
index 00000000..d4d9ec12
--- /dev/null
+++ b/src/animations.js
@@ -0,0 +1,2 @@
+// eslint-disable-next-line import/prefer-default-export
+export const mySpring = { type: 'spring', damping: 30, stiffness: 1000 };
diff --git a/src/renderer.jsx b/src/renderer.jsx
index 453c6eb9..4dd1a46e 100644
--- a/src/renderer.jsx
+++ b/src/renderer.jsx
@@ -1,9 +1,10 @@
import React, { memo, useEffect, useState, useCallback, useRef } from 'react';
import { IoIosHelpCircle, IoIosCamera } from 'react-icons/io';
-import { FaPlus, FaMinus, FaAngleLeft, FaAngleRight, FaTrashAlt, FaVolumeMute, FaVolumeUp } from 'react-icons/fa';
+import { FaPlus, FaMinus, FaAngleLeft, FaAngleRight, FaTrashAlt, FaVolumeMute, FaVolumeUp, FaYinYang } from 'react-icons/fa';
import { MdRotate90DegreesCcw } from 'react-icons/md';
+import { GiYinYang } from 'react-icons/gi';
import { FiScissors } from 'react-icons/fi';
-import { AnimatePresence } from 'framer-motion';
+import { AnimatePresence, motion } from 'framer-motion';
import { Popover, Button } from 'evergreen-ui';
import fromPairs from 'lodash/fromPairs';
@@ -13,6 +14,7 @@ import sortBy from 'lodash/sortBy';
import HelpSheet from './HelpSheet';
import TimelineSeg from './TimelineSeg';
+import InverseCutSegment from './InverseCutSegment';
import StreamsSelector from './StreamsSelector';
import { loadMifiLink } from './mifi';
@@ -82,7 +84,7 @@ const App = memo(() => {
const [currentTime, setCurrentTime] = useState();
const [duration, setDuration] = useState();
const [cutSegments, setCutSegments] = useState([createSegment()]);
- const [currentSeg, setCurrentSeg] = useState(0);
+ const [currentSegIndex, setCurrentSegIndex] = useState(0);
const [cutStartTimeManual, setCutStartTimeManual] = useState();
const [cutEndTimeManual, setCutEndTimeManual] = useState();
const [fileFormat, setFileFormat] = useState();
@@ -105,6 +107,7 @@ const App = memo(() => {
const [helpVisible, setHelpVisible] = useState(false);
const [timecodeShowFrames, setTimecodeShowFrames] = useState(false);
const [mifiLink, setMifiLink] = useState();
+ const [invertCutSegments, setInvertCutSegments] = useState(false);
const videoRef = useRef();
const timelineWrapperRef = useRef();
@@ -152,7 +155,7 @@ const App = memo(() => {
setWorking(false);
setPlaying(false);
setDuration();
- setCurrentSeg(0);
+ setCurrentSegIndex(0);
setCutSegments([createSegment()]);
setCutStartTimeManual();
setCutEndTimeManual();
@@ -167,6 +170,7 @@ const App = memo(() => {
setStreams([]);
setCopyStreamIds({});
setMuted(false);
+ setInvertCutSegments(false);
}, []);
useEffect(() => () => {
@@ -175,10 +179,10 @@ const App = memo(() => {
const frameRenderEnabled = !!(rotationPreviewRequested || dummyVideoPath);
- // Because segments could have undefined start / end (meaning extend to start of timeline or end duration)
+ // Because segments could have undefined start / end
+ // (meaning extend to start of timeline or end duration)
function getSegApparentStart(time) {
- if (time !== undefined) return time;
- return 0;
+ return time !== undefined ? time : 0;
}
const getSegApparentEnd = useCallback((time) => {
@@ -199,6 +203,9 @@ const App = memo(() => {
const haveInvalidSegs = invalidSegUuids.length > 0;
+ const currentCutSeg = cutSegments[currentSegIndex];
+ const currentApparentCutSeg = apparentCutSegments[currentSegIndex];
+
const inverseCutSegments = (() => {
if (haveInvalidSegs) return undefined;
if (apparentCutSegments.length < 1) return undefined;
@@ -242,20 +249,17 @@ const App = memo(() => {
const setCutTime = useCallback((type, time) => {
const cloned = clone(cutSegments);
- cloned[currentSeg][type] = time;
+ cloned[currentSegIndex][type] = time;
setCutSegments(cloned);
- }, [currentSeg, cutSegments]);
+ }, [currentSegIndex, cutSegments]);
function formatTimecode(sec) {
return formatDuration({ seconds: sec, fps: timecodeShowFrames ? detectedFps : undefined });
}
- const getCutSeg = useCallback((i) => cutSegments[i !== undefined ? i : currentSeg],
- [currentSeg, cutSegments]);
-
const addCutSegment = useCallback(() => {
- const cutStartTime = getCutSeg().start;
- const cutEndTime = getCutSeg().end;
+ const cutStartTime = currentCutSeg.start;
+ const cutEndTime = currentCutSeg.end;
if (cutStartTime === undefined && cutEndTime === undefined) return;
@@ -270,24 +274,24 @@ const App = memo(() => {
}),
];
- const currentSegNew = cutSegmentsNew.length - 1;
+ const currentSegIndexNew = cutSegmentsNew.length - 1;
setCutSegments(cutSegmentsNew);
- setCurrentSeg(currentSegNew);
+ setCurrentSegIndex(currentSegIndexNew);
}, [
- getCutSeg, cutSegments, currentTime, duration,
+ currentCutSeg, cutSegments, currentTime, duration,
]);
const setCutStart = useCallback(() => {
- const curSeg = getCutSeg();
// https://github.com/mifi/lossless-cut/issues/168
// If we are after the end of the last segment in the timeline,
// add a new segment that starts at currentTime
- if (curSeg.start != null && curSeg.end != null && currentTime > curSeg.end) {
+ if (currentCutSeg.start != null && currentCutSeg.end != null
+ && currentTime > currentCutSeg.end) {
addCutSegment();
} else {
setCutTime('start', currentTime);
}
- }, [setCutTime, currentTime, getCutSeg, addCutSegment]);
+ }, [setCutTime, currentTime, currentCutSeg, addCutSegment]);
const setCutEnd = useCallback(() => setCutTime('end', currentTime), [setCutTime, currentTime]);
@@ -356,13 +360,6 @@ const App = memo(() => {
setRotationPreviewRequested(true);
}
- const getApparentCutStartTimeOrCurrent = useCallback((i) => getSegApparentStart(getCutSeg(i).start), [getCutSeg]);
-
- const getApparentCutEndTimeOrCurrent = useCallback((i) => {
- const cutEndTime = getCutSeg(i).end;
- return getSegApparentEnd(cutEndTime);
- }, [getCutSeg, getSegApparentEnd]);
-
const offsetCurrentTime = (currentTime || 0) + startTimeOffset;
const mergeFiles = useCallback(async (paths) => {
@@ -400,15 +397,15 @@ const App = memo(() => {
if (cutSegments.length < 2) return;
const cutSegmentsNew = [...cutSegments];
- cutSegmentsNew.splice(currentSeg, 1);
+ cutSegmentsNew.splice(currentSegIndex, 1);
- const currentSegNew = Math.min(currentSeg, cutSegmentsNew.length - 1);
- setCurrentSeg(currentSegNew);
+ const currentSegIndexNew = Math.min(currentSegIndex, cutSegmentsNew.length - 1);
+ setCurrentSegIndex(currentSegIndexNew);
setCutSegments(cutSegmentsNew);
- }, [currentSeg, cutSegments]);
+ }, [currentSegIndex, cutSegments]);
- const jumpCutStart = () => seekAbs(getApparentCutStartTimeOrCurrent());
- const jumpCutEnd = () => seekAbs(getApparentCutEndTimeOrCurrent());
+ const jumpCutStart = () => seekAbs(currentApparentCutSeg.start);
+ const jumpCutEnd = () => seekAbs(currentApparentCutSeg.end);
function handleTap(e) {
const target = timelineWrapperRef.current;
@@ -462,14 +459,21 @@ const App = memo(() => {
return;
}
+ const segments = invertCutSegments ? inverseCutSegments : apparentCutSegments;
+
+ const ffmpegSegments = segments.map((seg) => ({
+ cutFrom: seg.start,
+ cutTo: seg.end,
+ }));
+
+ if (segments.length < 1) {
+ errorToast('No segments to export');
+ return;
+ }
+
try {
setWorking(true);
- const segments = cutSegments.map((seg, i) => ({
- cutFrom: getApparentCutStartTimeOrCurrent(i),
- cutTo: getApparentCutEndTimeOrCurrent(i),
- }));
-
const outFiles = await ffmpeg.cutMultiple({
customOutDir,
filePath,
@@ -478,7 +482,7 @@ const App = memo(() => {
rotation: effectiveRotation,
copyStreamIds,
keyframeCut,
- segments,
+ segments: ffmpegSegments,
onProgress: setCutProgress,
});
@@ -507,8 +511,8 @@ const App = memo(() => {
setWorking(false);
}
}, [
- effectiveRotation, getApparentCutStartTimeOrCurrent, getApparentCutEndTimeOrCurrent,
- working, cutSegments, duration, filePath, keyframeCut,
+ effectiveRotation, apparentCutSegments, invertCutSegments, inverseCutSegments,
+ working, duration, filePath, keyframeCut,
autoMerge, customOutDir, fileFormat, copyStreamIds, haveInvalidSegs,
]);
@@ -801,7 +805,7 @@ const App = memo(() => {
seekAbs(rel);
};
- const cutTime = type === 'start' ? getApparentCutStartTimeOrCurrent() : getApparentCutEndTimeOrCurrent();
+ const cutTime = type === 'start' ? currentApparentCutSeg.start : currentApparentCutSeg.end;
return (
{
const durationSafe = duration || 1;
const currentTimePos = currentTime !== undefined && `${(currentTime / durationSafe) * 100}%`;
- const segColor = (getCutSeg() || {}).color;
+ const segColor = (currentCutSeg || {}).color;
const segBgColor = segColor.alpha(0.5).string();
const jumpCutButtonStyle = {
@@ -882,13 +886,13 @@ const App = memo(() => {
type="button"
onClick={toggleAutoMerge}
>
- {autoMerge ? 'Auto merge segments to one file' : 'Export separate segments'}
+ {autoMerge ? 'Auto merge segments to one file' : 'Export separate files'}
- | Cut mode |
+ keyframe cut mode |
|
+
+ |
+ Discard (cut away) or keep selected segments from video when exporting
+ |
+
+
+ |
+
+
Delete audio?
@@ -953,6 +971,25 @@ const App = memo(() => {
const VolumeIcon = muted ? FaVolumeMute : FaVolumeUp;
+ function renderInvertCutButton() {
+ const KeepOrDiscardIcon = invertCutSegments ? GiYinYang : FaYinYang;
+
+ return (
+
+
+
+ setInvertCutSegments(v => !v))}
+ />
+
+
+
+ );
+ }
+
return (
@@ -976,10 +1013,10 @@ const App = memo(() => {
|