1
0
mirror of https://github.com/mifi/lossless-cut.git synced 2024-11-25 19:52:44 +01:00

implement undo/redo #176

This commit is contained in:
Mikael Finstad 2020-02-20 18:41:01 +08:00
parent 2aa0d1a8c6
commit e7d3de3a25
4 changed files with 70 additions and 40 deletions

View File

@ -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",

View File

@ -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');

View File

@ -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}
/>

View File

@ -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"