mirror of
https://github.com/mifi/lossless-cut.git
synced 2024-11-22 10:22:31 +01:00
implement undo/redo #176
This commit is contained in:
parent
2aa0d1a8c6
commit
e7d3de3a25
@ -78,7 +78,7 @@
|
||||
"react-icons": "^3.9.0",
|
||||
"react-lottie": "^1.2.3",
|
||||
"react-sortable-hoc": "^1.5.3",
|
||||
"react-use": "^13.24.0",
|
||||
"react-use": "^13.26.1",
|
||||
"read-chunk": "^2.0.0",
|
||||
"string-to-stream": "^1.1.1",
|
||||
"strong-data-uri": "^1.0.5",
|
||||
|
18
src/menu.js
18
src/menu.js
@ -74,6 +74,24 @@ module.exports = (app, mainWindow, newVersion) => {
|
||||
],
|
||||
};
|
||||
|
||||
const editSubMenu = menu.find(item => item.label === 'Edit').submenu;
|
||||
editSubMenu.splice(editSubMenu.findIndex(item => item.label === 'Undo'), 1, {
|
||||
label: 'Undo',
|
||||
accelerator: 'CmdOrCtrl+Z',
|
||||
click() {
|
||||
mainWindow.webContents.send('undo');
|
||||
},
|
||||
});
|
||||
|
||||
editSubMenu.splice(editSubMenu.findIndex(item => item.label === 'Redo'), 1, {
|
||||
label: 'Redo',
|
||||
accelerator: 'Shift+CmdOrCtrl+Z',
|
||||
click() {
|
||||
mainWindow.webContents.send('redo');
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
menu.splice((process.platform === 'darwin' ? 1 : 0), 0, fileMenu);
|
||||
|
||||
const helpIndex = menu.findIndex(item => item.role === 'help');
|
||||
|
@ -7,10 +7,11 @@ import { AnimatePresence, motion } from 'framer-motion';
|
||||
import Swal from 'sweetalert2';
|
||||
import Lottie from 'react-lottie';
|
||||
import { SideSheet, Button, Position } from 'evergreen-ui';
|
||||
import { useStateWithHistory } from 'react-use/lib/useStateWithHistory';
|
||||
|
||||
import fromPairs from 'lodash/fromPairs';
|
||||
import clamp from 'lodash/clamp';
|
||||
import clone from 'lodash/clone';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import flatMap from 'lodash/flatMap';
|
||||
|
||||
@ -89,10 +90,6 @@ const App = memo(() => {
|
||||
const [playing, setPlaying] = useState(false);
|
||||
const [playerTime, setPlayerTime] = useState();
|
||||
const [duration, setDuration] = useState();
|
||||
const [cutSegments, setCutSegments] = useState([createSegment()]);
|
||||
const [currentSegIndex, setCurrentSegIndex] = useState(0);
|
||||
const [cutStartTimeManual, setCutStartTimeManual] = useState();
|
||||
const [cutEndTimeManual, setCutEndTimeManual] = useState();
|
||||
const [fileFormat, setFileFormat] = useState();
|
||||
const [detectedFileFormat, setDetectedFileFormat] = useState();
|
||||
const [rotation, setRotation] = useState(360);
|
||||
@ -109,6 +106,16 @@ const App = memo(() => {
|
||||
const [commandedTime, setCommandedTime] = useState(0);
|
||||
const [ffmpegCommandLog, setFfmpegCommandLog] = useState([]);
|
||||
|
||||
// Segment related state
|
||||
const [currentSegIndex, setCurrentSegIndex] = useState(0);
|
||||
const [cutStartTimeManual, setCutStartTimeManual] = useState();
|
||||
const [cutEndTimeManual, setCutEndTimeManual] = useState();
|
||||
const [cutSegments, setCutSegments, cutSegmentsHistory] = useStateWithHistory(
|
||||
[createSegment()],
|
||||
100,
|
||||
);
|
||||
|
||||
|
||||
// Preferences
|
||||
const [captureFormat, setCaptureFormat] = useState(configStore.get('captureFormat'));
|
||||
useEffect(() => configStore.set('captureFormat', captureFormat), [captureFormat]);
|
||||
@ -194,8 +201,8 @@ const App = memo(() => {
|
||||
setWorking(false);
|
||||
setPlaying(false);
|
||||
setDuration();
|
||||
setCurrentSegIndex(0);
|
||||
setCutSegments([createSegment()]);
|
||||
cutSegmentsHistory.go(0);
|
||||
setCutSegments([createSegment()]); // TODO this will cause two history items
|
||||
setCutStartTimeManual();
|
||||
setCutEndTimeManual();
|
||||
setFileFormat();
|
||||
@ -211,7 +218,7 @@ const App = memo(() => {
|
||||
setCopyStreamIdsByFile({});
|
||||
setStreamsSelectorShown(false);
|
||||
setZoom(1);
|
||||
}, []);
|
||||
}, [cutSegmentsHistory, setCutSegments]);
|
||||
|
||||
useEffect(() => () => {
|
||||
if (dummyVideoPath) unlink(dummyVideoPath).catch(console.error);
|
||||
@ -245,8 +252,9 @@ const App = memo(() => {
|
||||
|
||||
const haveInvalidSegs = invalidSegUuids.length > 0;
|
||||
|
||||
const currentCutSeg = cutSegments[currentSegIndex];
|
||||
const currentApparentCutSeg = apparentCutSegments[currentSegIndex];
|
||||
const currentSegIndexSafe = Math.min(currentSegIndex, cutSegments.length - 1);
|
||||
const currentCutSeg = cutSegments[currentSegIndexSafe];
|
||||
const currentApparentCutSeg = apparentCutSegments[currentSegIndexSafe];
|
||||
const areWeCutting = apparentCutSegments.length > 1
|
||||
|| isCuttingStart(currentApparentCutSeg.start)
|
||||
|| isCuttingEnd(currentApparentCutSeg.end, duration);
|
||||
@ -294,17 +302,17 @@ const App = memo(() => {
|
||||
})();
|
||||
|
||||
const setCutTime = useCallback((type, time) => {
|
||||
const cloned = clone(cutSegments);
|
||||
const currentSeg = cloned[currentSegIndex];
|
||||
const cloned = cloneDeep(cutSegments);
|
||||
const currentSeg = currentCutSeg;
|
||||
if (type === 'start' && time >= getSegApparentEnd(currentSeg)) {
|
||||
throw new Error('Start time must precede end time');
|
||||
}
|
||||
if (type === 'end' && time <= getSegApparentStart(currentSeg)) {
|
||||
throw new Error('Start time must precede end time');
|
||||
}
|
||||
cloned[currentSegIndex][type] = time;
|
||||
cloned[currentSegIndexSafe][type] = time;
|
||||
setCutSegments(cloned);
|
||||
}, [currentSegIndex, getSegApparentEnd, cutSegments]);
|
||||
}, [currentSegIndexSafe, getSegApparentEnd, cutSegments, currentCutSeg, setCutSegments]);
|
||||
|
||||
function formatTimecode(sec) {
|
||||
return formatDuration({ seconds: sec, fps: timecodeShowFrames ? detectedFps : undefined });
|
||||
@ -330,11 +338,10 @@ const App = memo(() => {
|
||||
}),
|
||||
];
|
||||
|
||||
const currentSegIndexNew = cutSegmentsNew.length - 1;
|
||||
setCutSegments(cutSegmentsNew);
|
||||
setCurrentSegIndex(currentSegIndexNew);
|
||||
setCurrentSegIndex(cutSegmentsNew.length - 1);
|
||||
}, [
|
||||
currentCutSeg, cutSegments, getCurrentTime, duration,
|
||||
currentCutSeg, cutSegments, getCurrentTime, duration, setCutSegments,
|
||||
]);
|
||||
|
||||
const setCutStart = useCallback(() => {
|
||||
@ -494,12 +501,10 @@ const App = memo(() => {
|
||||
if (cutSegments.length < 2) return;
|
||||
|
||||
const cutSegmentsNew = [...cutSegments];
|
||||
cutSegmentsNew.splice(currentSegIndex, 1);
|
||||
cutSegmentsNew.splice(currentSegIndexSafe, 1);
|
||||
|
||||
const currentSegIndexNew = Math.min(currentSegIndex, cutSegmentsNew.length - 1);
|
||||
setCurrentSegIndex(currentSegIndexNew);
|
||||
setCutSegments(cutSegmentsNew);
|
||||
}, [currentSegIndex, cutSegments]);
|
||||
}, [currentSegIndexSafe, cutSegments, setCutSegments]);
|
||||
|
||||
const jumpCutStart = () => seekAbs(currentApparentCutSeg.start);
|
||||
const jumpCutEnd = () => seekAbs(currentApparentCutSeg.end);
|
||||
@ -961,12 +966,22 @@ const App = memo(() => {
|
||||
setStartTimeOffset(newStartTimeOffset);
|
||||
}
|
||||
|
||||
function undo() {
|
||||
cutSegmentsHistory.back();
|
||||
}
|
||||
|
||||
function redo() {
|
||||
cutSegmentsHistory.forward();
|
||||
}
|
||||
|
||||
electron.ipcRenderer.on('file-opened', fileOpened);
|
||||
electron.ipcRenderer.on('close-file', closeFile);
|
||||
electron.ipcRenderer.on('html5ify', html5ify);
|
||||
electron.ipcRenderer.on('show-merge-dialog', showOpenAndMergeDialog2);
|
||||
electron.ipcRenderer.on('set-start-offset', setStartOffset);
|
||||
electron.ipcRenderer.on('extract-all-streams', extractAllStreams);
|
||||
electron.ipcRenderer.on('undo', undo);
|
||||
electron.ipcRenderer.on('redo', redo);
|
||||
|
||||
return () => {
|
||||
electron.ipcRenderer.removeListener('file-opened', fileOpened);
|
||||
@ -975,10 +990,12 @@ const App = memo(() => {
|
||||
electron.ipcRenderer.removeListener('show-merge-dialog', showOpenAndMergeDialog2);
|
||||
electron.ipcRenderer.removeListener('set-start-offset', setStartOffset);
|
||||
electron.ipcRenderer.removeListener('extract-all-streams', extractAllStreams);
|
||||
electron.ipcRenderer.removeListener('undo', undo);
|
||||
electron.ipcRenderer.removeListener('redo', redo);
|
||||
};
|
||||
}, [
|
||||
load, mergeFiles, outputDir, filePath, customOutDir, startTimeOffset, getHtml5ifiedPath,
|
||||
createDummyVideo, resetState, extractAllStreams, userOpenFiles,
|
||||
createDummyVideo, resetState, extractAllStreams, userOpenFiles, cutSegmentsHistory,
|
||||
]);
|
||||
|
||||
async function showAddStreamSourceDialog() {
|
||||
@ -1455,7 +1472,7 @@ const App = memo(() => {
|
||||
segNum={i}
|
||||
color={seg.color}
|
||||
onSegClick={currentSegIndexNew => setCurrentSegIndex(currentSegIndexNew)}
|
||||
isActive={i === currentSegIndex}
|
||||
isActive={i === currentSegIndexSafe}
|
||||
duration={durationSafe}
|
||||
cutStart={seg.start}
|
||||
cutEnd={seg.end}
|
||||
@ -1575,7 +1592,7 @@ const App = memo(() => {
|
||||
size={30}
|
||||
style={{ margin: '0 5px', background: cutSegments.length < 2 ? undefined : segBgColor, borderRadius: 3, color: 'white' }}
|
||||
role="button"
|
||||
title={`Delete current segment ${currentSegIndex + 1}`}
|
||||
title={`Delete current segment ${currentSegIndexSafe + 1}`}
|
||||
onClick={removeCutSegment}
|
||||
/>
|
||||
|
||||
|
25
yarn.lock
25
yarn.lock
@ -260,10 +260,10 @@
|
||||
"@types/prop-types" "*"
|
||||
csstype "^2.2.0"
|
||||
|
||||
"@xobotyi/scrollbar-width@1.8.2":
|
||||
version "1.8.2"
|
||||
resolved "https://registry.yarnpkg.com/@xobotyi/scrollbar-width/-/scrollbar-width-1.8.2.tgz#056946ac41ade4885c576619c8d70c46c77e9683"
|
||||
integrity sha512-RV6+4hR29oMaPCvSYFUvzOvlsrg2s2k5NE9tNERs+4nFIC9dRXxs+lL2CcaRTbl3yQxKwAZ8Cd+qMI8aUu9TFw==
|
||||
"@xobotyi/scrollbar-width@1.9.0":
|
||||
version "1.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.0.tgz#2a5d02f15c7f5624339e5d690aba432bfd9e79f0"
|
||||
integrity sha512-W8oNXd3HkW9eQHxk+47iRx4aqd0yIV9NoeykUTd0uE0sYx3LOAQE7rfHOd8xtMP7IADfLIdG0o0H1sXvHUF7dw==
|
||||
|
||||
abbrev@1:
|
||||
version "1.1.1"
|
||||
@ -4846,11 +4846,6 @@ react-event-listener@^0.5.1:
|
||||
prop-types "^15.6.0"
|
||||
warning "^3.0.0"
|
||||
|
||||
react-fast-compare@^2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
|
||||
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
|
||||
|
||||
react-hammerjs@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/react-hammerjs/-/react-hammerjs-1.0.1.tgz#bc1ed9e9ef7da057163fb169ce12917b6d6ca7d8"
|
||||
@ -4919,18 +4914,18 @@ react-transition-group@^2.5.0:
|
||||
prop-types "^15.6.2"
|
||||
react-lifecycles-compat "^3.0.4"
|
||||
|
||||
react-use@^13.24.0:
|
||||
version "13.24.0"
|
||||
resolved "https://registry.yarnpkg.com/react-use/-/react-use-13.24.0.tgz#f4574e26cfaaad65e3f04c0d5ff80c1836546236"
|
||||
integrity sha512-p8GsZuMdz8OeIGzuYLm6pzJysKOhNyQjCUG6SHrQGk6o6ghy/RVGSqnmxVacNbN9166S0+9FsM1N1yH9GzWlgg==
|
||||
react-use@^13.26.1:
|
||||
version "13.26.1"
|
||||
resolved "https://registry.yarnpkg.com/react-use/-/react-use-13.26.1.tgz#a26e51b26ebe1a3a00cadfe4d7f15c25bb19780b"
|
||||
integrity sha512-hDc4s8w4WI8G7c1BX+IsrdQFcZPfCHE/6oLpGPtcIPoxVhwj4QvVmNE8RnsnddBJ57HN8Xvkc3jp/8Z/4OB53w==
|
||||
dependencies:
|
||||
"@types/js-cookie" "2.2.4"
|
||||
"@xobotyi/scrollbar-width" "1.8.2"
|
||||
"@xobotyi/scrollbar-width" "1.9.0"
|
||||
copy-to-clipboard "^3.2.0"
|
||||
fast-deep-equal "^3.1.1"
|
||||
fast-shallow-equal "^1.0.0"
|
||||
js-cookie "^2.2.1"
|
||||
nano-css "^5.2.1"
|
||||
react-fast-compare "^2.0.4"
|
||||
resize-observer-polyfill "^1.5.1"
|
||||
screenfull "^5.0.0"
|
||||
set-harmonic-interval "^1.0.1"
|
||||
|
Loading…
Reference in New Issue
Block a user